// Package password 提供 OpenLDAP userPassword 的哈希编码。 // // 为什么需要它:slapd 对客户端通过普通 Modify 写入的 userPassword **不做** // 自动哈希(除非启用 ppolicy 的 hash_cleartext),因此如果直接写明文, // 目录里就会以明文形式保存密码。这里在写入前自行编码成 {SSHA}, // 不依赖服务端配置。 package password import ( "crypto/rand" "crypto/sha1" "encoding/base64" "fmt" ) // EncodeSSHA 把明文密码编码成 OpenLDAP 的 {SSHA} 形式。 // // SSHA = base64( SHA1(密码 + 盐) + 盐 )。附带随机盐可以避免彩虹表, // 并让相同密码产生不同哈希。 func EncodeSSHA(plain string) (string, error) { salt := make([]byte, 8) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("生成盐失败: %w", err) } h := sha1.New() h.Write([]byte(plain)) h.Write(salt) sum := h.Sum(nil) // 哈希后拼接盐,一并 base64 out := append(sum, salt...) return "{SSHA}" + base64.StdEncoding.EncodeToString(out), nil } // VerifySSHA 校验明文密码与 {SSHA} 哈希是否匹配。 // // 主要用于测试,也可供将来的密码校验功能复用。 func VerifySSHA(encoded, plain string) bool { const prefix = "{SSHA}" if len(encoded) <= len(prefix) || encoded[:len(prefix)] != prefix { return false } raw, err := base64.StdEncoding.DecodeString(encoded[len(prefix):]) if err != nil || len(raw) < sha1.Size { return false } sum := raw[:sha1.Size] salt := raw[sha1.Size:] h := sha1.New() h.Write([]byte(plain)) h.Write(salt) want := h.Sum(nil) if len(want) != len(sum) { return false } // 恒定时间比较,避免时序侧信道(虽然此处主要用于测试, // 保持与生产代码一致的比较方式更不易被误用)。 var diff byte for i := range want { diff |= want[i] ^ sum[i] } return diff == 0 } // Random 生成一个用户不可知的随机初始密码。 // // 用户不需要知道它:Authelia 的密码重置流程不需要旧密码, // 因此新账号首次登录应走“忘记密码”。这比为所有人设同一个初始密码安全得多。 func Random() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("生成随机密码失败: %w", err) } return base64.RawURLEncoding.EncodeToString(b), nil }