Smail/backend/utils/credential/encryption_test.go
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

162 lines
4.3 KiB
Go

package credential
import (
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
)
func TestEncryptDecrypt(t *testing.T) {
root := t.TempDir()
encrypted, err := Encrypt(root, "secret-value")
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(encrypted, encryptedPrefix) || strings.Contains(encrypted, "secret-value") {
t.Fatal("credential was not encrypted")
}
decrypted, err := Decrypt(root, encrypted)
if err != nil || decrypted != "secret-value" {
t.Fatal("credential could not be decrypted")
}
info, err := os.Stat(filepath.Join(root, "config", "credential.key"))
if err != nil {
t.Fatal(err)
}
if runtime.GOOS != "windows" && info.Mode().Perm()&0077 != 0 {
t.Fatalf("credential key permissions are too broad: %o", info.Mode().Perm())
}
}
func TestDecryptRejectsTampering(t *testing.T) {
root := t.TempDir()
encrypted, err := Encrypt(root, "secret-value")
if err != nil {
t.Fatal(err)
}
replacement := byte('A')
if encrypted[len(encrypted)-1] == replacement {
replacement = 'B'
}
encrypted = encrypted[:len(encrypted)-1] + string(replacement)
if _, err := Decrypt(root, encrypted); err == nil {
t.Fatal("tampered credential was accepted")
}
}
func TestDecryptRejectsPlaintext(t *testing.T) {
if _, err := Decrypt(t.TempDir(), "legacy-plaintext-secret"); err == nil {
t.Fatal("plaintext credential was accepted by strict decryption")
}
}
func TestEncryptKeepsEmptyAndProtectsPrefixValues(t *testing.T) {
root := t.TempDir()
if value, err := Encrypt(root, ""); err != nil || value != "" {
t.Fatal("empty credential changed")
}
plaintext := "enc:v1:third-party-secret"
encrypted, err := Encrypt(root, plaintext)
if err != nil {
t.Fatal(err)
}
if encrypted == plaintext || !strings.HasPrefix(encrypted, encryptedPrefix) {
t.Fatal("prefix credential was not encrypted")
}
decrypted, err := Decrypt(root, encrypted)
if err != nil || decrypted != plaintext {
t.Fatal("prefix credential did not round trip")
}
}
func TestCredentialKeyTemporaryNamesAreIndependentRandomValues(t *testing.T) {
seen := make(map[string]struct{})
for i := 0; i < 32; i++ {
name, err := newCredentialKeyTemporaryName()
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(name, ".credential.key-") {
t.Fatalf("unexpected temporary name: %s", name)
}
if _, ok := seen[name]; ok {
t.Fatalf("duplicate temporary name: %s", name)
}
seen[name] = struct{}{}
}
}
func TestReadKeyRejectsDirectorySymlink(t *testing.T) {
root := t.TempDir()
target := t.TempDir()
if err := os.WriteFile(filepath.Join(target, "credential.key"), make([]byte, 32), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, filepath.Join(root, "config")); err != nil {
t.Skipf("symlink is unavailable: %v", err)
}
if _, err := readKey(root); err == nil {
t.Fatal("credential directory symlink was accepted")
}
}
func TestReadKeyRejectsKeySymlink(t *testing.T) {
root := t.TempDir()
configDir := filepath.Join(root, "config")
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatal(err)
}
target := filepath.Join(root, "outside.key")
if err := os.WriteFile(target, make([]byte, 32), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, filepath.Join(configDir, "credential.key")); err != nil {
t.Skipf("symlink is unavailable: %v", err)
}
if _, err := readKey(root); err == nil {
t.Fatal("credential key symlink was accepted")
}
}
func TestConcurrentEncryptUsesSingleCompleteKey(t *testing.T) {
root := t.TempDir()
const workers = 16
values := make(chan string, workers)
errors := make(chan error, workers)
var group sync.WaitGroup
for i := 0; i < workers; i++ {
group.Add(1)
go func() {
defer group.Done()
value, err := Encrypt(root, "shared-secret")
if err != nil {
errors <- err
return
}
values <- value
}()
}
group.Wait()
close(values)
close(errors)
for err := range errors {
t.Fatal(err)
}
for value := range values {
plaintext, err := Decrypt(root, value)
if err != nil || plaintext != "shared-secret" {
t.Fatalf("concurrent credential could not be decrypted: %v", err)
}
}
entries, err := os.ReadDir(filepath.Join(root, "config"))
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Name() != "credential.key" {
t.Fatalf("unexpected credential directory contents: %#v", entries)
}
}