Files
MyGoNavi/internal/app/connection_package_appkey_test.go
tianqijiuyun-latiao 52d2ee7592 feat(connection-package): 支持连接恢复包双模式加密导入导出
- 新增 v2 连接恢复包 appKey 与文件密码双模式加密链路
- 扩展前后端导入导出流程并兼容 v1 与 legacy 格式
- 修复无文件密码恢复包导入误弹密码框导致的流程阻塞
2026-04-11 23:51:43 +08:00

142 lines
4.7 KiB
Go

package app
import (
"encoding/base64"
"reflect"
"strings"
"testing"
)
func TestDeriveConnectionPackageAppKeyIsStable(t *testing.T) {
originalSeed := connectionPackageAppKeySeed
originalSalt := connectionPackageAppKeySalt
t.Cleanup(func() {
connectionPackageAppKeySeed = originalSeed
connectionPackageAppKeySalt = originalSalt
resetConnectionPackageAppKeyCache()
})
connectionPackageAppKeySeed = "unit-test-seed"
connectionPackageAppKeySalt = "unit-test-salt"
resetConnectionPackageAppKeyCache()
first, err := deriveConnectionPackageAppKey()
if err != nil {
t.Fatalf("deriveConnectionPackageAppKey returned error: %v", err)
}
second, err := deriveConnectionPackageAppKey()
if err != nil {
t.Fatalf("deriveConnectionPackageAppKey returned error on second call: %v", err)
}
if len(first) != connectionPackageAES256KeyBytes {
t.Fatalf("expected %d-byte app key, got %d", connectionPackageAES256KeyBytes, len(first))
}
if !reflect.DeepEqual(first, second) {
t.Fatal("expected deriveConnectionPackageAppKey to be stable across repeated calls")
}
connectionPackageAppKeySeed = "unit-test-seed-rotated"
resetConnectionPackageAppKeyCache()
rotated, err := deriveConnectionPackageAppKey()
if err != nil {
t.Fatalf("deriveConnectionPackageAppKey returned error after seed rotation: %v", err)
}
if reflect.DeepEqual(first, rotated) {
t.Fatal("expected different injected seed to produce a different app key")
}
}
func TestEncryptSecretFieldRoundTrip(t *testing.T) {
appKey := []byte("0123456789abcdef0123456789abcdef")
encrypted, err := encryptSecretField(appKey, "super-secret", "conn-1")
if err != nil {
t.Fatalf("encryptSecretField returned error: %v", err)
}
if strings.HasPrefix(encrypted, "ENC:") {
t.Fatalf("encrypted field must not carry ENC prefix, got %q", encrypted)
}
raw, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
t.Fatalf("encrypted field must be base64, got error: %v", err)
}
if len(raw) <= connectionPackageNonceBytes {
t.Fatalf("expected nonce+ciphertext output, got %d bytes", len(raw))
}
decrypted, err := decryptSecretField(appKey, encrypted, "conn-1")
if err != nil {
t.Fatalf("decryptSecretField returned error: %v", err)
}
if decrypted != "super-secret" {
t.Fatalf("round-trip mismatch: got %q", decrypted)
}
}
func TestDecryptSecretFieldRejectsAADMismatch(t *testing.T) {
appKey := []byte("0123456789abcdef0123456789abcdef")
encrypted, err := encryptSecretField(appKey, "super-secret", "conn-1")
if err != nil {
t.Fatalf("encryptSecretField returned error: %v", err)
}
if _, err := decryptSecretField(appKey, encrypted, "conn-2"); err == nil {
t.Fatal("expected decryptSecretField to reject mismatched AAD")
}
}
func TestEncryptSecretBundleRoundTripAndAADBinding(t *testing.T) {
appKey := []byte("0123456789abcdef0123456789abcdef")
plain := connectionSecretBundle{
Password: "primary-secret",
SSHPassword: "ssh-secret",
ProxyPassword: "proxy-secret",
HTTPTunnelPassword: "http-secret",
MySQLReplicaPassword: "mysql-secret",
MongoReplicaPassword: "mongo-secret",
OpaqueURI: "postgres://user:pass@db.local/app",
OpaqueDSN: "server=db.local;password=secret",
}
encrypted, err := encryptSecretBundle(appKey, plain, "conn-1")
if err != nil {
t.Fatalf("encryptSecretBundle returned error: %v", err)
}
for name, value := range map[string]string{
"password": encrypted.Password,
"sshPassword": encrypted.SSHPassword,
"proxyPassword": encrypted.ProxyPassword,
"httpTunnelPassword": encrypted.HTTPTunnelPassword,
"mysqlReplicaPassword": encrypted.MySQLReplicaPassword,
"mongoReplicaPassword": encrypted.MongoReplicaPassword,
"opaqueURI": encrypted.OpaqueURI,
"opaqueDSN": encrypted.OpaqueDSN,
} {
if value == "" {
t.Fatalf("expected encrypted %s field to be populated", name)
}
if strings.HasPrefix(value, "ENC:") {
t.Fatalf("encrypted %s field must not carry ENC prefix", name)
}
if value == plain.Password || value == plain.SSHPassword || value == plain.ProxyPassword ||
value == plain.HTTPTunnelPassword || value == plain.MySQLReplicaPassword || value == plain.MongoReplicaPassword ||
value == plain.OpaqueURI || value == plain.OpaqueDSN {
t.Fatalf("expected encrypted %s field to differ from plaintext", name)
}
}
decrypted, err := decryptSecretBundle(appKey, encrypted, "conn-1")
if err != nil {
t.Fatalf("decryptSecretBundle returned error: %v", err)
}
if !reflect.DeepEqual(decrypted, plain) {
t.Fatalf("bundle round-trip mismatch: got=%+v want=%+v", decrypted, plain)
}
if _, err := decryptSecretBundle(appKey, encrypted, "conn-2"); err == nil {
t.Fatal("expected decryptSecretBundle to reject mismatched connection AAD")
}
}