Files
openteam/server/internal/pkg/crypto/crypto_test.go
T
Sakurasan 5d6e5bdb04 feat: 主密钥诊断与迁移命令;空/占位主密钥启动警告
渠道上游 key 用 OT_MASTER_KEY 经 AES-GCM 加密落库,主密钥一旦变化,全部渠道在运行时
报 "failed to decrypt channel key"。最常见的成因是历史实例先用了空主密钥(退化为
sha256("")),后来才补上 OT_MASTER_KEY——旧密文从此解不开,看起来像渠道全挂了。

- 新增 `check-channels`:打印主密钥派生路径与指纹(SHA-256 前 4 字节,不泄露密钥)
  + 逐渠道解密结果,区分「全部失败=主密钥不对」与「个别失败=该渠道密文异常」;
  存在失败时退出码为 1,便于脚本告警
- 新增 `rotate-master-key`:旧密钥解密、新密钥重新加密,全部渠道单事务更新,
  任一渠道解不开就整体回滚(--force 可跳过),避免库里新旧密钥混杂;
  写入前对新密文做回读校验;--dry-run 只验证不改库。空主密钥作为默认 --old,
  正是「先空后补」的历史路径;配套迁移无需重新录入任何渠道 key
- crypto 新增 KeyFingerprint / KeyMode:指纹用于比对两个实例是否同一把密钥,
  KeyMode 显式暴露「16/24/32 字节用原始字节、其他长度走 SHA-256 派生」这条
  容易踩的长度陷阱(改长度等于换密钥)
- 启动警告:开发环境不拦空/占位主密钥,但明确提示它等于公开密钥,并给出
  rotate-master-key 指引;生产环境仍由 config.validate 直接拒绝启动
- README 补主密钥与渠道密钥运维小节
- 测试:换密钥必解密失败、长度切换 32↔33 字节必失败、指纹一致性

验证:go vet、go test ./... 全绿;端到端复现「空主密钥建渠道 → 配新主密钥全部解密失败
→ rotate-master-key 迁移 → check-channels 全部正常」,明文长度与前后缀保持一致。
2026-11-08 23:12:17 +08:00

100 lines
3.1 KiB
Go
Raw 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 crypto
import (
"strings"
"testing"
)
func TestPasswordHashRoundTrip(t *testing.T) {
h := NewPasswordHasher(1, 64*1024, 1, 32, 16)
hash, err := h.HashPassword("s3cret-password")
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
ok, err := h.VerifyPassword(hash, "s3cret-password")
if err != nil || !ok {
t.Fatalf("VerifyPassword correct: ok=%v err=%v", ok, err)
}
ok, _ = h.VerifyPassword(hash, "wrong-password")
if ok {
t.Fatal("VerifyPassword accepted wrong password")
}
}
func TestEncryptorRoundTrip(t *testing.T) {
e := NewEncryptor("a-very-long-master-key-1234567890")
enc, err := e.Encrypt("sk-upstream-secret")
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if enc == "sk-upstream-secret" {
t.Fatal("ciphertext equals plaintext")
}
plain, err := e.Decrypt(enc)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if plain != "sk-upstream-secret" {
t.Fatalf("round trip mismatch: %q", plain)
}
}
func TestEncryptorShortKeyDerived(t *testing.T) {
// 短主密钥应派生 32 字节而非报错
e := NewEncryptor("short")
if _, err := e.Encrypt("x"); err != nil {
t.Fatalf("Encrypt with short key: %v", err)
}
}
// 主密钥换过(值不同)必然解不开:这是 "failed to decrypt channel key" 的根因。
func TestEncryptorWrongKeyFails(t *testing.T) {
a := NewEncryptor("master-key-AAAAAAAAAAAAAAAAAAAAAAAA")
b := NewEncryptor("master-key-BBBBBBBBBBBBBBBBBBBBBBBB")
enc, err := a.Encrypt("sk-upstream-secret")
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if _, err := b.Decrypt(enc); err == nil {
t.Fatal("换了主密钥仍能解密:加解密强度有问题")
}
}
// 长度陷阱:32 字节走原始字节,33 字节走 SHA-256 派生,两者是不同的密钥。
func TestEncryptorKeyLengthModeSwitch(t *testing.T) {
raw32 := "0123456789abcdef0123456789abcdef" // 32 字节
derived := raw32 + "x" // 33 字节 → 派生
enc, err := NewEncryptor(raw32).Encrypt("secret")
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if _, err := NewEncryptor(derived).Decrypt(enc); err == nil {
t.Fatal("长度变化后仍在用同一把密钥:派生路径切换应导致解密失败")
}
if got := NewEncryptor(raw32).KeyMode(); got == "" || !strings.Contains(got, "raw-32") {
t.Fatalf("32 字节应报 raw-32,实际 %q", got)
}
if got := NewEncryptor(derived).KeyMode(); !strings.Contains(got, "derived-32") {
t.Fatalf("33 字节应报 derived-32,实际 %q", got)
}
}
// 指纹只用于比对,不泄露密钥;同密钥同指纹、异密钥异指纹。
func TestEncryptorKeyFingerprint(t *testing.T) {
a1 := NewEncryptor("same-master-key-1234567890")
a2 := NewEncryptor("same-master-key-1234567890")
b := NewEncryptor("other-master-key-123456789")
if a1.KeyFingerprint() != a2.KeyFingerprint() {
t.Fatal("同一主密钥的指纹应一致")
}
if a1.KeyFingerprint() == b.KeyFingerprint() {
t.Fatal("不同主密钥的指纹不应相同")
}
if len(a1.KeyFingerprint()) != 8 {
t.Fatalf("指纹应为 4 字节 hex(8 字符),实际 %q", a1.KeyFingerprint())
}
if strings.Contains(a1.KeyFingerprint(), "same-master-key") {
t.Fatal("指纹不应包含密钥内容")
}
}