4a8b4a2bc4
- Add test/crypto/ (19 tests): AES-256-GCM encrypt/decrypt, PBKDF2, IsEncrypted, HashPassword - Add test/knownhosts/ (14 tests): TOFU verify, MITM detection, CRUD, persistence - Add test/storage/ (20 tests): KeyPair/Snippet CRUD, encryption, MergeStrategy - Add test/config/ (9 tests): config lifecycle, path getters - Add test/tui/ (10 tests): WrapFooter, ClampWidth, TruncateStr - Fix knownhosts deadlock: Add/Remove use saveInternal() - Export responsive.go functions for testing - Add docs/TEST_PLAN.md with full scenario documentation Coverage: crypto 0%→100%, knownhosts 0%→100%, storage 40%→90%, config 22%→80%, tui 40%→70%
237 lines
6.7 KiB
Go
237 lines
6.7 KiB
Go
package crypto_test
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/crypto"
|
|
)
|
|
|
|
// 1.1 DeriveKey determinism — same password + salt → same key
|
|
func TestDeriveKeyDeterminism(t *testing.T) {
|
|
salt := []byte("1234567890123456")
|
|
key1 := crypto.DeriveKey("password", salt)
|
|
key2 := crypto.DeriveKey("password", salt)
|
|
if !bytes.Equal(key1, key2) {
|
|
t.Error("DeriveKey should return same key for same password + salt")
|
|
}
|
|
}
|
|
|
|
// 1.2 DeriveKey password variation — different password → different key
|
|
func TestDeriveKeyPasswordVariation(t *testing.T) {
|
|
salt := []byte("1234567890123456")
|
|
key1 := crypto.DeriveKey("password1", salt)
|
|
key2 := crypto.DeriveKey("password2", salt)
|
|
if bytes.Equal(key1, key2) {
|
|
t.Error("DeriveKey should return different keys for different passwords")
|
|
}
|
|
}
|
|
|
|
// 1.3 DeriveKey salt variation — different salt → different key
|
|
func TestDeriveKeySaltVariation(t *testing.T) {
|
|
key1 := crypto.DeriveKey("password", []byte("1234567890123456"))
|
|
key2 := crypto.DeriveKey("password", []byte("6543210987654321"))
|
|
if bytes.Equal(key1, key2) {
|
|
t.Error("DeriveKey should return different keys for different salts")
|
|
}
|
|
}
|
|
|
|
// 1.4 DeriveKey empty password — no panic, valid key length
|
|
func TestDeriveKeyEmptyPassword(t *testing.T) {
|
|
salt := []byte("1234567890123456")
|
|
key := crypto.DeriveKey("", salt)
|
|
if len(key) != crypto.KeyLength {
|
|
t.Errorf("DeriveKey with empty password should return %d bytes, got %d", crypto.KeyLength, len(key))
|
|
}
|
|
}
|
|
|
|
// 1.5 Encrypt/Decrypt round-trip
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
plaintext := []byte("hello world")
|
|
encoded, err := crypto.Encrypt(plaintext, "mypassword")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
decoded, err := crypto.Decrypt(encoded, "mypassword")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
if !bytes.Equal(plaintext, decoded) {
|
|
t.Errorf("Round-trip failed: got %q, want %q", decoded, plaintext)
|
|
}
|
|
}
|
|
|
|
// 1.6 Encrypt empty plaintext
|
|
func TestEncryptDecryptEmpty(t *testing.T) {
|
|
plaintext := []byte("")
|
|
encoded, err := crypto.Encrypt(plaintext, "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
decoded, err := crypto.Decrypt(encoded, "password")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
if len(decoded) != 0 {
|
|
t.Errorf("Expected empty plaintext, got %d bytes", len(decoded))
|
|
}
|
|
}
|
|
|
|
// 1.7 Encrypt large data (1MB)
|
|
func TestEncryptDecryptLargeData(t *testing.T) {
|
|
plaintext := make([]byte, 1024*1024)
|
|
if _, err := rand.Read(plaintext); err != nil {
|
|
t.Fatalf("Failed to generate random data: %v", err)
|
|
}
|
|
encoded, err := crypto.Encrypt(plaintext, "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
decoded, err := crypto.Decrypt(encoded, "password")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
if !bytes.Equal(plaintext, decoded) {
|
|
t.Error("Large data round-trip failed")
|
|
}
|
|
}
|
|
|
|
// 1.8 Encrypt unicode
|
|
func TestEncryptDecryptUnicode(t *testing.T) {
|
|
plaintext := []byte("こんにちは世界 🌍")
|
|
encoded, err := crypto.Encrypt(plaintext, "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
decoded, err := crypto.Decrypt(encoded, "password")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
if !bytes.Equal(plaintext, decoded) {
|
|
t.Errorf("Unicode round-trip failed: got %q, want %q", decoded, plaintext)
|
|
}
|
|
}
|
|
|
|
// 1.9 Encrypt with newlines
|
|
func TestEncryptDecryptNewlines(t *testing.T) {
|
|
plaintext := []byte("line1\nline2\nline3")
|
|
encoded, err := crypto.Encrypt(plaintext, "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
decoded, err := crypto.Decrypt(encoded, "password")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
if !bytes.Equal(plaintext, decoded) {
|
|
t.Errorf("Newlines round-trip failed: got %q, want %q", decoded, plaintext)
|
|
}
|
|
}
|
|
|
|
// 1.10 Wrong password → error
|
|
func TestDecryptWrongPassword(t *testing.T) {
|
|
plaintext := []byte("secret data")
|
|
encoded, err := crypto.Encrypt(plaintext, "correct-password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
_, err = crypto.Decrypt(encoded, "wrong-password")
|
|
if err == nil {
|
|
t.Error("Decrypt with wrong password should return error")
|
|
}
|
|
}
|
|
|
|
// 1.11 Empty password → error
|
|
func TestDecryptEmptyPassword(t *testing.T) {
|
|
plaintext := []byte("secret data")
|
|
encoded, err := crypto.Encrypt(plaintext, "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
_, err = crypto.Decrypt(encoded, "")
|
|
if err == nil {
|
|
t.Error("Decrypt with empty password should return error")
|
|
}
|
|
}
|
|
|
|
// 1.12 IsEncrypted valid ciphertext
|
|
func TestIsEncryptedValid(t *testing.T) {
|
|
encoded, err := crypto.Encrypt([]byte("test"), "password")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
if !crypto.IsEncrypted(encoded) {
|
|
t.Error("IsEncrypted should return true for valid ciphertext")
|
|
}
|
|
}
|
|
|
|
// 1.13 IsEncrypted plaintext
|
|
func TestIsEncryptedPlaintext(t *testing.T) {
|
|
if crypto.IsEncrypted("hello world") {
|
|
t.Error("IsEncrypted should return false for plaintext")
|
|
}
|
|
}
|
|
|
|
// 1.14 IsEncrypted empty
|
|
func TestIsEncryptedEmpty(t *testing.T) {
|
|
if crypto.IsEncrypted("") {
|
|
t.Error("IsEncrypted should return false for empty string")
|
|
}
|
|
}
|
|
|
|
// 1.15 HashPassword determinism
|
|
func TestHashPasswordDeterminism(t *testing.T) {
|
|
hash1 := crypto.HashPassword("mypassword")
|
|
hash2 := crypto.HashPassword("mypassword")
|
|
if hash1 != hash2 {
|
|
t.Error("HashPassword should return same hash for same password")
|
|
}
|
|
}
|
|
|
|
// 1.16 HashPassword variation
|
|
func TestHashPasswordVariation(t *testing.T) {
|
|
hash1 := crypto.HashPassword("password1")
|
|
hash2 := crypto.HashPassword("password2")
|
|
if hash1 == hash2 {
|
|
t.Error("HashPassword should return different hashes for different passwords")
|
|
}
|
|
}
|
|
|
|
// 1.17 Encrypt randomness — same input → different ciphertext
|
|
func TestEncryptRandomness(t *testing.T) {
|
|
plaintext := []byte("same input")
|
|
encoded1, _ := crypto.Encrypt(plaintext, "password")
|
|
encoded2, _ := crypto.Encrypt(plaintext, "password")
|
|
if encoded1 == encoded2 {
|
|
t.Error("Encrypt should produce different ciphertext each time (random salt)")
|
|
}
|
|
}
|
|
|
|
// Verify constants
|
|
func TestConstants(t *testing.T) {
|
|
if crypto.KeyLength != 32 {
|
|
t.Errorf("KeyLength = %d, want 32", crypto.KeyLength)
|
|
}
|
|
if crypto.SaltLength != 16 {
|
|
t.Errorf("SaltLength = %d, want 16", crypto.SaltLength)
|
|
}
|
|
if crypto.Iterations != 100000 {
|
|
t.Errorf("Iterations = %d, want 100000", crypto.Iterations)
|
|
}
|
|
}
|
|
|
|
// Verify error variables
|
|
func TestErrorVars(t *testing.T) {
|
|
if crypto.ErrInvalidPassword == nil {
|
|
t.Error("ErrInvalidPassword should not be nil")
|
|
}
|
|
if crypto.ErrDecryptionFailed == nil {
|
|
t.Error("ErrDecryptionFailed should not be nil")
|
|
}
|
|
if !strings.Contains(crypto.ErrDecryptionFailed.Error(), "decryption failed") {
|
|
t.Error("ErrDecryptionFailed message should contain 'decryption failed'")
|
|
}
|
|
}
|