chore: 初始化 idsync 项目

飞书通讯录 ↔ LDAP 同步工具的首个提交。

- internal/feishu: 飞书通讯录客户端、快照与重试
- internal/ldaplib: LDAP 读写与批量操作
- internal/mapping: 飞书部门/用户到 LDAP 条目的映射
- internal/syncer: 同步计划生成、执行与调度
- internal/store: SQLite 台账
- internal/server + internal/webui: 管理后台与内嵌前端产物
- internal/password: SSHA 口令处理
- config.example.yml / Dockerfile / Makefile / web 前端源码

真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
This commit is contained in:
2026-09-29 00:10:59 +08:00
commit 947b1a1475
43 changed files with 10079 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
// 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
}