// Package crypto 密码哈希(argon2id)与对称加密(AES-GCM)。 package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // PasswordHasher argon2id 参数(来自配置)。 type PasswordHasher struct { Time uint32 Memory uint32 Threads uint8 KeyLen uint32 SaltLen int } func NewPasswordHasher(time, memory uint32, threads uint8, keyLen uint32, saltLen int) *PasswordHasher { return &PasswordHasher{Time: time, Memory: memory, Threads: threads, KeyLen: keyLen, SaltLen: saltLen} } // HashPassword 编码为 $argon2id$v=19$m=...,t=...,p=...$salt$hash func (h *PasswordHasher) HashPassword(password string) (string, error) { salt := make([]byte, h.SaltLen) if _, err := rand.Read(salt); err != nil { return "", err } key := argon2.IDKey([]byte(password), salt, h.Time, h.Memory, h.Threads, h.KeyLen) enc := base64.RawStdEncoding return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", h.Memory, h.Time, h.Threads, enc.EncodeToString(salt), enc.EncodeToString(key)), nil } // VerifyPassword 校验密码,常数时间比较。 func (h *PasswordHasher) VerifyPassword(encoded, password string) (bool, error) { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, errors.New("invalid hash format") } var memory, time uint32 var threads uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { return false, err } enc := base64.RawStdEncoding salt, err := enc.DecodeString(parts[4]) if err != nil { return false, err } want, err := enc.DecodeString(parts[5]) if err != nil { return false, err } got := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil } // --------------------------------------------------------------------------- // AES-GCM 渠道密钥加密 // Encryptor 用主密钥加解密渠道上游 key。 type Encryptor struct { key []byte mode string // 派生路径说明(排障用) } // NewEncryptor 主密钥必须为 16/24/32 字节;不足时用 SHA-256 派生固定 32 字节。 // 长度陷阱:16/24/32 字节直接用原始字节,其他长度走 SHA-256 派生——把主密钥改成 // 另一个长度(或从 32 字节改成 33 字符)就切换到另一条路径,等价于换了一把密钥, // 历史密文会全部解不开。 func NewEncryptor(master string) *Encryptor { key := []byte(master) mode := "" switch len(key) { case 16: mode = "raw-16(AES-128,直接用主密钥原始字节)" case 24: mode = "raw-24(AES-192,直接用主密钥原始字节)" case 32: mode = "raw-32(AES-256,直接用主密钥原始字节)" default: key = sha256Sum(master) mode = fmt.Sprintf("derived-32(主密钥 %d 字节,经 SHA-256 派生)", len(master)) } return &Encryptor{key: key, mode: mode} } // KeyMode 说明这把主密钥走的是哪条派生路径(排障用,不泄露密钥)。 func (e *Encryptor) KeyMode() string { return e.mode } // KeyFingerprint 派生后 AES 密钥的指纹(SHA-256 前 4 字节 hex)。 // 只用于确认「两个实例是不是同一把主密钥」:指纹相同必然能互相解密, // 不同必然解不开,且不泄露密钥本身。 func (e *Encryptor) KeyFingerprint() string { sum := sha256.Sum256(e.key) return hex.EncodeToString(sum[:4]) } // Encrypt 输出 base64(nonce || ciphertext) func (e *Encryptor) Encrypt(plain string) (string, error) { block, err := aes.NewCipher(e.key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", err } ct := gcm.Seal(nil, nonce, []byte(plain), nil) return base64.StdEncoding.EncodeToString(append(nonce, ct...)), nil } // Decrypt 解析 Encrypt 的输出。 func (e *Encryptor) Decrypt(enc string) (string, error) { raw, err := base64.StdEncoding.DecodeString(enc) if err != nil { return "", err } block, err := aes.NewCipher(e.key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } if len(raw) < gcm.NonceSize() { return "", errors.New("ciphertext too short") } nonce, ct := raw[:gcm.NonceSize()], raw[gcm.NonceSize():] plain, err := gcm.Open(nil, nonce, ct, nil) if err != nil { return "", err } return string(plain), nil }