Files
Havline/internal/agent/apr1.go
T
mrlee 166449871f feat(agent): Basic Auth 改为纯 Go apr1 并去掉 VPS openssl 依赖
- 新增 internal/agent/apr1.go(Apache apr_md5_encode 移植)+ 参考向量单元测试(与 openssl 输出逐字节一致)
- ensureBasicAuthSecret / writeBasicAuthSecret 不再调用 openssl passwd -apr1
- 安装脚本 token 改用 /dev/urandom(内嵌副本与 scripts/agent-install.sh 同步)
- AgentVersion 0.9.2;重编 amd64/arm64 内嵌二进制
- CHANGELOG v1.2.0 补记该改动
2026-09-23 11:22:11 +08:00

102 lines
2.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"crypto/md5"
"crypto/rand"
"fmt"
"strings"
)
// apr1Itoa64 是 apr1(Apache MD5 crypt)使用的自定义 base64 字母表,同时用作随机盐字符集。
const apr1Itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
// apr1Crypt 生成 apr1 哈希,与 `openssl passwd -apr1` 等价(纯 Go,无外部命令依赖)。
// 选 apr1 而非 bcrypt:Nginx 对 $apr1$ 是自带实现,跨发行版稳定;bcrypt 依赖系统 crypt(3),
// 在 CentOS 7 这类老 glibc 上无法校验。
func apr1Crypt(password string) (string, error) {
salt, err := apr1RandomSalt(8)
if err != nil {
return "", err
}
return apr1CryptWithSalt(password, salt), nil
}
// apr1RandomSalt 生成指定长度的随机盐(字符取自 itoa64,与 openssl 行为一致)。
func apr1RandomSalt(n int) (string, error) {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("生成随机盐失败: %w", err)
}
var b strings.Builder
for _, v := range buf {
b.WriteByte(apr1Itoa64[int(v)%len(apr1Itoa64)])
}
return b.String(), nil
}
// apr1CryptWithSalt 是 apr1 的确定性实现(salt 取前 8 字符),供单元测试用参考向量比对。
// 算法来自 Apache apr_md5_encode:MD5 三轮 + 1000 次迭代 + 自定义字节序 base64。
func apr1CryptWithSalt(password, salt string) string {
if len(salt) > 8 {
salt = salt[:8]
}
ctx := md5.New()
ctx.Write([]byte(password))
ctx.Write([]byte("$apr1$"))
ctx.Write([]byte(salt))
alt := md5.Sum([]byte(password + salt + password))
for i := len(password); i > 0; i -= 16 {
n := i
if n > 16 {
n = 16
}
ctx.Write(alt[:n])
}
for i := len(password); i > 0; i >>= 1 {
if i&1 != 0 {
ctx.Write([]byte{0})
} else {
ctx.Write([]byte(password[0:1]))
}
}
final := ctx.Sum(nil)
for i := 0; i < 1000; i++ {
round := md5.New()
if i&1 != 0 {
round.Write([]byte(password))
} else {
round.Write(final)
}
if i%3 != 0 {
round.Write([]byte(salt))
}
if i%7 != 0 {
round.Write([]byte(password))
}
if i&1 != 0 {
round.Write(final)
} else {
round.Write([]byte(password))
}
final = round.Sum(nil)
}
var out strings.Builder
to64 := func(v uint32, n int) {
for ; n > 0; n-- {
out.WriteByte(apr1Itoa64[v&0x3f])
v >>= 6
}
}
to64(uint32(final[0])<<16|uint32(final[6])<<8|uint32(final[12]), 4)
to64(uint32(final[1])<<16|uint32(final[7])<<8|uint32(final[13]), 4)
to64(uint32(final[2])<<16|uint32(final[8])<<8|uint32(final[14]), 4)
to64(uint32(final[3])<<16|uint32(final[9])<<8|uint32(final[15]), 4)
to64(uint32(final[4])<<16|uint32(final[10])<<8|uint32(final[5]), 4)
to64(uint32(final[11]), 2)
return "$apr1$" + salt + "$" + out.String()
}