前端: - src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector) - 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用 - index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲) 后端分层增强(保留包名): - 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave) - 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验) - 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突) - models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors) v2 优化移植与缺陷修复: - 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固 - 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷 - 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown) - 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐 - 补 crypto/jwt/repository 单元测试(含并发写回归) 验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过 Co-Authored-By: Claude <noreply@anthropic.com>
79 lines
2.4 KiB
Go
79 lines
2.4 KiB
Go
package crypto
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestEncryptDecryptRoundTrip 覆盖 AES-GCM 加解密往返(含空串与 Unicode)。
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
cases := []string{"", "hello", "a-much-longer-secret-with-unicode-密码-🔑"}
|
|
for _, plain := range cases {
|
|
enc, err := Encrypt(plain)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt(%q) error: %v", plain, err)
|
|
}
|
|
dec, err := Decrypt(enc)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt(%q) error: %v", plain, err)
|
|
}
|
|
if dec != plain {
|
|
t.Fatalf("round-trip mismatch: got %q want %q", dec, plain)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDecryptTamperedFails 验证 GCM 认证:篡改密文必须解密失败。
|
|
func TestDecryptTamperedFails(t *testing.T) {
|
|
enc, err := Encrypt("secret-value")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(enc) < 4 {
|
|
t.Fatal("ciphertext too short")
|
|
}
|
|
// 翻转末尾两个 base64 字符,破坏 GCM 认证标签
|
|
tampered := enc[:len(enc)-2] + "AA"
|
|
if _, err := Decrypt(tampered); err == nil {
|
|
t.Fatal("expected Decrypt to fail on tampered ciphertext, got nil")
|
|
}
|
|
}
|
|
|
|
// TestEncryptFieldContract 是 M2 的回归保护:
|
|
// EncryptField 绝不能返回 "ENC_ERR:" 哨兵(否则 DecryptField 会把它当明文凭证用),
|
|
// 只能返回 ""(空输入)或 "ENC:" 前缀;且与 DecryptField 可往返。
|
|
func TestEncryptFieldContract(t *testing.T) {
|
|
enc, err := EncryptField("super-secret")
|
|
if err != nil {
|
|
t.Fatalf("EncryptField error: %v", err)
|
|
}
|
|
if !strings.HasPrefix(enc, "ENC:") {
|
|
t.Fatalf("EncryptField must return ENC:-prefixed value, got %q", enc)
|
|
}
|
|
if enc == "ENC_ERR:" {
|
|
t.Fatal("EncryptField returned legacy sentinel ENC_ERR: (M2 regression)")
|
|
}
|
|
if got := DecryptField(enc); got != "super-secret" {
|
|
t.Fatalf("DecryptField round-trip: got %q want super-secret", got)
|
|
}
|
|
if !IsEncryptedField(enc) {
|
|
t.Fatal("IsEncryptedField should be true for ENC: value")
|
|
}
|
|
if IsEncryptedField("plain-value") {
|
|
t.Fatal("IsEncryptedField should be false for plain value")
|
|
}
|
|
// 空输入约定
|
|
if enc, err := EncryptField(""); enc != "" || err != nil {
|
|
t.Fatalf("EncryptField(\"\") = (%q,%v), want (\"\",nil)", enc, err)
|
|
}
|
|
}
|
|
|
|
// TestEncryptFieldRandomNonce 同一明文两次加密应产生不同密文(随机 nonce)。
|
|
func TestEncryptFieldRandomNonce(t *testing.T) {
|
|
a, _ := EncryptField("same-secret")
|
|
b, _ := EncryptField("same-secret")
|
|
if a == b {
|
|
t.Fatal("expected different ciphertexts due to random nonce")
|
|
}
|
|
}
|