- 新增 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 补记该改动
102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
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()
|
||
}
|