Config.Validate now parses every age_recipients entry with age.ParseX25519Recipient, so a bad recipient fails at config load instead of deep in a backup after the snapshot row and tree walk. On failure the error names the position (age_recipients[N]) and never the value: a recipient string can itself be a secret key an operator pasted by mistake, and age's own error quotes its input. An entry starting with AGE-SECRET-KEY- gets a specific message. The remaining parse sites (blobgen.NewWriter, crypto NewEncryptor and UpdateRecipients), reachable by callers that skip config.Load, likewise drop the value and age's wrapped error, naming only the position. Model: opus-4-8
199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
package crypto_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
"sneak.berlin/go/vaultik/internal/crypto"
|
|
)
|
|
|
|
func TestEncryptor(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Generate a test key pair
|
|
identity, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate identity: %v", err)
|
|
}
|
|
|
|
publicKey := identity.Recipient().String()
|
|
|
|
// Create encryptor
|
|
enc, err := crypto.NewEncryptor([]string{publicKey})
|
|
if err != nil {
|
|
t.Fatalf("failed to create encryptor: %v", err)
|
|
}
|
|
|
|
// Test data
|
|
plaintext := []byte("Hello, World! This is a test message.")
|
|
|
|
// Encrypt
|
|
ciphertext, err := enc.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt: %v", err)
|
|
}
|
|
|
|
// Verify it's actually encrypted (should be larger and different)
|
|
if bytes.Equal(plaintext, ciphertext) {
|
|
t.Error("ciphertext equals plaintext")
|
|
}
|
|
|
|
// Decrypt to verify
|
|
r, err := age.Decrypt(bytes.NewReader(ciphertext), identity)
|
|
if err != nil {
|
|
t.Fatalf("failed to decrypt: %v", err)
|
|
}
|
|
|
|
var decrypted bytes.Buffer
|
|
|
|
_, err = decrypted.ReadFrom(r)
|
|
if err != nil {
|
|
t.Fatalf("failed to read decrypted data: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(plaintext, decrypted.Bytes()) {
|
|
t.Error("decrypted data doesn't match original")
|
|
}
|
|
}
|
|
|
|
func TestEncryptorMultipleRecipients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Generate three test key pairs
|
|
identity1, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate identity1: %v", err)
|
|
}
|
|
|
|
identity2, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate identity2: %v", err)
|
|
}
|
|
|
|
identity3, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate identity3: %v", err)
|
|
}
|
|
|
|
publicKeys := []string{
|
|
identity1.Recipient().String(),
|
|
identity2.Recipient().String(),
|
|
identity3.Recipient().String(),
|
|
}
|
|
|
|
// Create encryptor with multiple recipients
|
|
enc, err := crypto.NewEncryptor(publicKeys)
|
|
if err != nil {
|
|
t.Fatalf("failed to create encryptor: %v", err)
|
|
}
|
|
|
|
// Test data
|
|
plaintext := []byte("Secret message for multiple recipients")
|
|
|
|
// Encrypt
|
|
ciphertext, err := enc.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt: %v", err)
|
|
}
|
|
|
|
// Verify each recipient can decrypt
|
|
identities := []age.Identity{identity1, identity2, identity3}
|
|
for i, identity := range identities {
|
|
r, err := age.Decrypt(bytes.NewReader(ciphertext), identity)
|
|
if err != nil {
|
|
t.Fatalf("recipient %d failed to decrypt: %v", i+1, err)
|
|
}
|
|
|
|
var decrypted bytes.Buffer
|
|
|
|
_, err = decrypted.ReadFrom(r)
|
|
if err != nil {
|
|
t.Fatalf("recipient %d failed to read decrypted data: %v", i+1, err)
|
|
}
|
|
|
|
if !bytes.Equal(plaintext, decrypted.Bytes()) {
|
|
t.Errorf("recipient %d: decrypted data doesn't match original", i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncryptorUpdateRecipients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Generate two identities
|
|
identity1, _ := age.GenerateX25519Identity()
|
|
identity2, _ := age.GenerateX25519Identity()
|
|
|
|
publicKey1 := identity1.Recipient().String()
|
|
publicKey2 := identity2.Recipient().String()
|
|
|
|
// Create encryptor with first key
|
|
enc, err := crypto.NewEncryptor([]string{publicKey1})
|
|
if err != nil {
|
|
t.Fatalf("failed to create encryptor: %v", err)
|
|
}
|
|
|
|
// Encrypt with first key
|
|
plaintext := []byte("test data")
|
|
|
|
ciphertext1, err := enc.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt: %v", err)
|
|
}
|
|
|
|
// Update to second key
|
|
err = enc.UpdateRecipients([]string{publicKey2})
|
|
if err != nil {
|
|
t.Fatalf("failed to update recipients: %v", err)
|
|
}
|
|
|
|
// Encrypt with second key
|
|
ciphertext2, err := enc.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt: %v", err)
|
|
}
|
|
|
|
// First ciphertext should only decrypt with first identity
|
|
_, err = age.Decrypt(bytes.NewReader(ciphertext1), identity1)
|
|
if err != nil {
|
|
t.Error("failed to decrypt with identity1")
|
|
}
|
|
|
|
_, err = age.Decrypt(bytes.NewReader(ciphertext1), identity2)
|
|
if err == nil {
|
|
t.Error("should not decrypt with identity2")
|
|
}
|
|
|
|
// Second ciphertext should only decrypt with second identity
|
|
_, err = age.Decrypt(bytes.NewReader(ciphertext2), identity2)
|
|
if err != nil {
|
|
t.Error("failed to decrypt with identity2")
|
|
}
|
|
|
|
_, err = age.Decrypt(bytes.NewReader(ciphertext2), identity1)
|
|
if err == nil {
|
|
t.Error("should not decrypt with identity1")
|
|
}
|
|
}
|
|
|
|
// TestNewEncryptorSecretKeyNotEchoed verifies that a secret key mistakenly
|
|
// passed as a recipient does not appear in the returned error. A recipient
|
|
// string can be sensitive, so the error must name only the position.
|
|
func TestNewEncryptorSecretKeyNotEchoed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
secretKey := "AGE-SECRET-KEY-19CR5YSFW59HM4TLD6GX" +
|
|
"VEDMZFTVVF7PPHKUT68TXSFPK7APHXA2QS2NJA5"
|
|
|
|
_, err := crypto.NewEncryptor([]string{secretKey})
|
|
if err == nil {
|
|
t.Fatal("NewEncryptor returned nil, want error")
|
|
}
|
|
|
|
if strings.Contains(err.Error(), secretKey) {
|
|
t.Fatalf("error echoed the recipient value: %v", err)
|
|
}
|
|
}
|