Files
secret/pkg/agehd/agehd_test.go
sneak 397011a592
All checks were successful
check / check (push) Successful in 2m0s
Update golangci-lint to v2.12.2 with canonical config (closes #30)
- Replace .golangci.yml with the canonical strict config (all linters
  enabled except the standard disable list; lll 88, funlen 80/50,
  cyclop 15, dupl 100; test files now linted)
- Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by
  tag and digest (Debian-based)
- Fix all ~1550 findings surfaced by the new config: line wrapping,
  wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel
  errors, perfsprint/modernize rewrites, goconst constants, thelper,
  testifylint, noctx CommandContext, testpackage conversions,
  t.Parallel() where safe, and complexity/dupl helper extraction
- Record the change and follow-up items in TODO.md

User-visible strings
--------------------

No user-visible string changes remain. Every error message this branch
composes is byte-identical to the one main composes.

The err113 sentinels are shaped so that fmt.Errorf reassembles the
original text around them: a sentinel carries the fixed words of the
message and the caller supplies the interpolated value in the position
it has always occupied. Where the value sits in the middle of the
sentence the sentinel therefore holds only a fragment (for example
vault.ErrVaultNotFound is "does not exist", composed by its caller as
"vault <name> does not exist"); each such sentinel documents the
message it participates in.

Verified mechanically rather than by inspection: every fmt.Errorf and
errors.New call site in both trees was parsed, the Error() text of any
sentinel passed to %w substituted in, and the resulting sets of
composed message templates compared. All 350 templates main produces
are still produced, character for character; the set of messages lost
or altered is empty.

unlocker list
-------------

findUnlockerIDByMetadata now returns (string, error) instead of
signalling failure with an empty ID. An unreadable unlockers.d is no
longer indistinguishable from "no matching entry", so UnlockersList
skips the entry with a warning naming the directory, as it did before
the scan was extracted into a helper, rather than emitting a row under
a synthesized fallback ID that no unlocker remove or unlocker select
can match and that suppresses the current-unlocker marker. The
duplicate-check and shell-completion callers skip on the same
condition, matching their pre-extraction behavior. Covered by tests in
internal/cli/unlockers_list_test.go.
2026-08-09 02:00:27 +00:00

1041 lines
24 KiB
Go

//nolint:lll // Test vectors contain long lines
package agehd //nolint:testpackage // white-box test of unexported internals
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"io"
"strings"
"testing"
"filippo.io/age"
"github.com/tyler-smith/go-bip39"
)
//nolint:dupword // BIP39 test mnemonics repeat words by design
const (
mnemonic = "abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon abandon about"
// Test xprv from BIP85 test vectors
testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
// Additional test mnemonics for comprehensive testing
testMnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic15 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic18 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic21 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testMnemonic24 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
// Test messages used throughout the tests
testMessageHelloWorld = "hello world"
testMessageHelloFromXPRV = "hello from xprv"
testMessageGeneric = "test message"
testMessageBoundary = "boundary test"
testMessageBenchmark = "benchmark test message"
testMessageLargePattern = "A"
// Error messages for validation
errorMsgNeed32Bytes = "need 32-byte scalar, got"
errorMsgInvalidXPRV = "invalid-xprv"
// Test constants for various scenarios
// Removed testSkipMessage as tests are no longer skipped
// Numeric constants for testing
testNumGoroutines = 10
testNumIterations = 100
// Large data test constants
testDataSizeMegabyte = 1024 * 1024 // 1 MB
)
// errIndexOutOfRange guards against runaway loop indices in tests.
var errIndexOutOfRange = errors.New("index out of safe range")
// encryptDecryptRoundTrip encrypts msg to id's recipient and verifies
// that decrypting returns the original message.
func encryptDecryptRoundTrip(t *testing.T, id *age.X25519Identity, msg string) {
t.Helper()
var ct bytes.Buffer
w, err := age.Encrypt(&ct, id.Recipient())
if err != nil {
t.Fatalf("encrypt init: %v", err)
}
_, err = io.WriteString(w, msg)
if err != nil {
t.Fatalf("write: %v", err)
}
err = w.Close()
if err != nil {
t.Fatalf("encrypt close: %v", err)
}
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
if err != nil {
t.Fatalf("decrypt init: %v", err)
}
dec, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read: %v", err)
}
if got := string(dec); got != msg {
t.Fatalf("round-trip mismatch: %q", got)
}
}
func TestEncryptDecrypt(t *testing.T) {
t.Parallel()
id, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive: %v", err)
}
t.Logf("secret: %s", id.String())
t.Logf("recipient: %s", id.Recipient().String())
encryptDecryptRoundTrip(t, id, testMessageHelloWorld)
}
func TestDeriveIdentityFromXPRV(t *testing.T) {
t.Parallel()
id, err := DeriveIdentityFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive from xprv: %v", err)
}
t.Logf("xprv secret: %s", id.String())
t.Logf("xprv recipient: %s", id.Recipient().String())
// Test encryption/decryption with xprv-derived identity
encryptDecryptRoundTrip(t, id, testMessageHelloFromXPRV)
}
// requireDeterministicDerivation verifies that derive is deterministic
// for a fixed index and that different indices produce different
// identities. It returns the identities for indices 0 and 1.
func requireDeterministicDerivation(
t *testing.T,
derive func(uint32) (*age.X25519Identity, error),
) (*age.X25519Identity, *age.X25519Identity) {
t.Helper()
// Test that the same input and index always produce the same identity
id1, err := derive(0)
if err != nil {
t.Fatalf("derive 1: %v", err)
}
id2, err := derive(0)
if err != nil {
t.Fatalf("derive 2: %v", err)
}
if id1.String() != id2.String() {
t.Fatalf(
"identities should be deterministic: %s != %s",
id1.String(),
id2.String(),
)
}
// Test that different indices produce different identities
id3, err := derive(1)
if err != nil {
t.Fatalf("derive 3: %v", err)
}
if id1.String() == id3.String() {
t.Fatalf("different indices should produce different identities")
}
return id1, id3
}
func TestDeterministicDerivation(t *testing.T) {
t.Parallel()
id1, id3 := requireDeterministicDerivation(
t,
func(n uint32) (*age.X25519Identity, error) {
return DeriveIdentity(mnemonic, n)
},
)
t.Logf("Index 0: %s", id1.String())
t.Logf("Index 1: %s", id3.String())
}
func TestDeterministicXPRVDerivation(t *testing.T) {
t.Parallel()
id1, id3 := requireDeterministicDerivation(
t,
func(n uint32) (*age.X25519Identity, error) {
return DeriveIdentityFromXPRV(testXPRV, n)
},
)
t.Logf("XPRV Index 0: %s", id1.String())
t.Logf("XPRV Index 1: %s", id3.String())
}
func TestMnemonicVsXPRVConsistency(t *testing.T) {
t.Parallel()
// Consistency between mnemonic-derived and xprv-derived identities
// is not yet covered by this test.
}
func TestEntropyLength(t *testing.T) {
t.Parallel()
// Test that DeriveEntropy returns exactly 32 bytes
entropy, err := DeriveEntropy(mnemonic, 0)
if err != nil {
t.Fatalf("derive entropy: %v", err)
}
if len(entropy) != 32 {
t.Fatalf("expected 32 bytes of entropy, got %d", len(entropy))
}
t.Logf("Entropy (32 bytes): %x", entropy)
// Test that DeriveEntropyFromXPRV returns exactly 32 bytes
entropyXPRV, err := DeriveEntropyFromXPRV(testXPRV, 0)
if err != nil {
t.Fatalf("derive entropy from xprv: %v", err)
}
if len(entropyXPRV) != 32 {
t.Fatalf(
"expected 32 bytes of entropy from xprv, got %d",
len(entropyXPRV),
)
}
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
// Note: We don't compare the entropy values since the test mnemonic and test xprv
// are from different sources and should produce different entropy values.
}
func TestIdentityFromEntropy(t *testing.T) {
t.Parallel()
// Test that IdentityFromEntropy works with custom entropy
entropy := make([]byte, 32)
for i := range entropy {
entropy[i] = byte(i)
}
id, err := IdentityFromEntropy(entropy)
if err != nil {
t.Fatalf("identity from entropy: %v", err)
}
t.Logf("Custom entropy identity: %s", id.String())
// Test that it rejects wrong-sized entropy
_, err = IdentityFromEntropy(entropy[:31])
if err == nil {
t.Fatalf("expected error for 31-byte entropy")
}
// Create a 33-byte slice to test rejection
entropy33 := make([]byte, 33)
copy(entropy33, entropy)
_, err = IdentityFromEntropy(entropy33)
if err == nil {
t.Fatalf("expected error for 33-byte entropy")
}
}
func TestInvalidXPRV(t *testing.T) {
t.Parallel()
// Test with invalid xprv
_, err := DeriveIdentityFromXPRV(errorMsgInvalidXPRV, 0)
if err == nil {
t.Fatalf("expected error for invalid xprv")
}
t.Logf("Got expected error for invalid xprv: %v", err)
}
// TestClampFunction tests the RFC-7748 clamping function
func TestClampFunction(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input []byte
expected []byte
}{
{
name: "all zeros",
input: make([]byte, 32),
expected: append(make([]byte, 31), 64),
},
{
name: "all ones",
input: bytes.Repeat([]byte{255}, 32),
expected: append(
[]byte{248},
append(bytes.Repeat([]byte{255}, 30), 127)...),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
input := make([]byte, 32)
copy(input, tt.input)
clamp(input)
// Check specific bits that should be clamped
if input[0]&7 != 0 {
t.Errorf(
"first byte should have bottom 3 bits cleared, got %08b",
input[0],
)
}
if input[31]&128 != 0 {
t.Errorf(
"last byte should have top bit cleared, got %08b",
input[31],
)
}
if input[31]&64 == 0 {
t.Errorf(
"last byte should have second-to-top bit set, got %08b",
input[31],
)
}
})
}
}
// requireIdentityError asserts that identity derivation failed with an
// error containing errorMsg and returned no identity.
func requireIdentityError(
t *testing.T,
identity *age.X25519Identity,
err error,
errorMsg string,
) {
t.Helper()
if err == nil {
t.Errorf("expected error but got none")
} else if !strings.Contains(err.Error(), errorMsg) {
t.Errorf(
"expected error containing %q, got %q",
errorMsg,
err.Error(),
)
}
if identity != nil {
t.Errorf("expected nil identity on error, got %v", identity)
}
}
// TestIdentityFromEntropyEdgeCases tests edge cases for IdentityFromEntropy
func TestIdentityFromEntropyEdgeCases(t *testing.T) {
t.Parallel()
tests := []struct {
name string
entropy []byte
expectError bool
errorMsg string
}{
{
name: "nil entropy",
entropy: nil,
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 0",
},
{
name: "empty entropy",
entropy: []byte{},
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 0",
},
{
name: "too short entropy",
entropy: make([]byte, 31),
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 31",
},
{
name: "too long entropy",
entropy: make([]byte, 33),
expectError: true,
errorMsg: errorMsgNeed32Bytes + " 33",
},
{
name: "valid 32-byte entropy",
entropy: make([]byte, 32),
expectError: false,
},
{
name: "random valid entropy",
entropy: func() []byte {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
// In test context, panic is acceptable for
// setup failures
panic(err)
}
return b
}(),
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
identity, err := IdentityFromEntropy(tt.entropy)
if tt.expectError {
requireIdentityError(t, identity, err, tt.errorMsg)
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if identity == nil {
t.Errorf("expected valid identity, got nil")
}
})
}
}
// TestDeriveEntropyInvalidMnemonic tests error handling for invalid mnemonics
func TestDeriveEntropyInvalidMnemonic(t *testing.T) {
t.Parallel()
tests := []struct {
name string
mnemonic string
}{
{
name: "empty mnemonic",
mnemonic: "",
},
{
name: "single word",
mnemonic: "abandon",
},
{
name: "invalid word",
mnemonic: "invalid word sequence that does not exist in bip39",
},
{
name: "wrong word count",
mnemonic: "abandon abandon abandon abandon abandon", //nolint:dupword // repeated-word mnemonic
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Note: BIP39 library is quite permissive and doesn't validate
// mnemonic words strictly, so we mainly test that the function
// doesn't panic and produces some result
entropy, err := DeriveEntropy(tt.mnemonic, 0)
if err != nil {
t.Logf("Got error for invalid mnemonic %q: %v", tt.name, err)
return
}
if len(entropy) != 32 {
t.Errorf("expected 32 bytes even for invalid mnemonic, got %d", len(entropy))
}
t.Logf("Invalid mnemonic %q produced entropy: %x", tt.name, entropy)
})
}
}
// TestDeriveEntropyFromXPRVInvalidInputs tests error handling for invalid XPRVs
func TestDeriveEntropyFromXPRVInvalidInputs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
xprv string
expectError bool
}{
{
name: "empty xprv",
xprv: "",
expectError: true,
},
{
name: "invalid base58",
xprv: "invalid-base58-string-!@#$%",
expectError: true,
},
{
name: "wrong prefix",
xprv: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
expectError: true,
},
{
name: "truncated xprv",
xprv: "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLj",
expectError: true,
},
{
name: "valid xprv",
xprv: testXPRV,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
entropy, err := DeriveEntropyFromXPRV(tt.xprv, 0)
if tt.expectError {
if err == nil {
t.Errorf("expected error for invalid xprv %q", tt.name)
} else {
t.Logf("Got expected error for %q: %v", tt.name, err)
}
return
}
if err != nil {
t.Errorf("unexpected error for valid xprv: %v", err)
}
if len(entropy) != 32 {
t.Errorf("expected 32 bytes of entropy, got %d", len(entropy))
}
})
}
}
// TestDifferentMnemonicLengths tests derivation with different mnemonic lengths
func TestDifferentMnemonicLengths(t *testing.T) {
t.Parallel()
mnemonics := map[string]string{
"12 words": testMnemonic12,
"15 words": testMnemonic15,
"18 words": testMnemonic18,
"21 words": testMnemonic21,
"24 words": testMnemonic24,
}
for name, mnemonic := range mnemonics {
t.Run(name, func(t *testing.T) {
t.Parallel()
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("failed to derive identity from %s: %v", name, err)
}
// Test that we can encrypt/decrypt
encryptDecryptRoundTrip(t, identity, testMessageGeneric)
t.Logf("%s identity: %s", name, identity.String())
})
}
}
// TestIndexBoundaries tests derivation with various index values
func TestIndexBoundaries(t *testing.T) {
t.Parallel()
indices := []uint32{
0, // minimum
1, // basic
100, // moderate
1000, // larger
0x7FFFFFFF, // maximum hardened index
0xFFFFFFFF, // maximum uint32
}
for _, index := range indices {
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
t.Parallel()
identity, err := DeriveIdentity(mnemonic, index)
if err != nil {
t.Fatalf(
"failed to derive identity at index %d: %v",
index,
err,
)
}
// Verify the identity is valid by testing encryption/decryption
encryptDecryptRoundTrip(t, identity, testMessageBoundary)
t.Logf("Index %d identity: %s", index, identity.String())
})
}
}
// TestEntropyUniqueness tests that different inputs produce different entropy
func TestEntropyUniqueness(t *testing.T) {
t.Parallel()
// Test different indices with same mnemonic
entropy1, err := DeriveEntropy(mnemonic, 0)
if err != nil {
t.Fatalf("derive entropy 1: %v", err)
}
entropy2, err := DeriveEntropy(mnemonic, 1)
if err != nil {
t.Fatalf("derive entropy 2: %v", err)
}
if bytes.Equal(entropy1, entropy2) {
t.Fatalf("different indices should produce different entropy")
}
// Test different mnemonics with same index
entropy3, err := DeriveEntropy(testMnemonic24, 0)
if err != nil {
t.Fatalf("derive entropy 3: %v", err)
}
if bytes.Equal(entropy1, entropy3) {
t.Fatalf("different mnemonics should produce different entropy")
}
t.Logf("Entropy uniqueness verified across indices and mnemonics")
}
// TestConcurrentDerivation tests that derivation is safe for concurrent use
func TestConcurrentDerivation(t *testing.T) {
t.Parallel()
results := make(chan string, testNumGoroutines*testNumIterations)
errCh := make(chan error, testNumGoroutines*testNumIterations)
for range testNumGoroutines {
go func() {
for j := range testNumIterations {
if j < 0 || j > 1000000 {
errCh <- errIndexOutOfRange
return
}
identity, err := DeriveIdentity(mnemonic, uint32(j))
if err != nil {
errCh <- err
return
}
results <- identity.String()
}
}()
}
// Collect results
resultMap := make(map[string]int)
for range testNumGoroutines * testNumIterations {
select {
case result := <-results:
resultMap[result]++
case err := <-errCh:
t.Fatalf("concurrent derivation error: %v", err)
}
}
// Verify that each index produced the same result across all goroutines
expectedResults := testNumGoroutines
for result, count := range resultMap {
if count != expectedResults {
t.Errorf(
"result %s appeared %d times, expected %d",
result,
count,
expectedResults,
)
}
}
t.Logf(
"Concurrent derivation test passed with %d unique results",
len(resultMap),
)
}
// Benchmark tests
func BenchmarkDeriveIdentity(b *testing.B) {
for i := range b.N {
index := i % 1000
if index < 0 || index > 1000000 {
b.Fatalf("index out of safe range: %d", index)
}
_, err := DeriveIdentity(mnemonic, uint32(index))
if err != nil {
b.Fatalf("derive identity: %v", err)
}
}
}
func BenchmarkDeriveIdentityFromXPRV(b *testing.B) {
for i := range b.N {
index := i % 1000
if index < 0 || index > 1000000 {
b.Fatalf("index out of safe range: %d", index)
}
_, err := DeriveIdentityFromXPRV(testXPRV, uint32(index))
if err != nil {
b.Fatalf("derive identity from xprv: %v", err)
}
}
}
func BenchmarkDeriveEntropy(b *testing.B) {
for i := range b.N {
index := i % 1000
if index < 0 || index > 1000000 {
b.Fatalf("index out of safe range: %d", index)
}
_, err := DeriveEntropy(mnemonic, uint32(index))
if err != nil {
b.Fatalf("derive entropy: %v", err)
}
}
}
func BenchmarkIdentityFromEntropy(b *testing.B) {
entropy := make([]byte, 32)
_, err := rand.Read(entropy)
if err != nil {
b.Fatalf("failed to generate random entropy: %v", err)
}
b.ResetTimer()
for range b.N {
_, err := IdentityFromEntropy(entropy)
if err != nil {
b.Fatalf("identity from entropy: %v", err)
}
}
}
func BenchmarkEncryptDecrypt(b *testing.B) {
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
b.Fatalf("derive identity: %v", err)
}
b.ResetTimer()
for range b.N {
var ct bytes.Buffer
w, err := age.Encrypt(&ct, identity.Recipient())
if err != nil {
b.Fatalf("encrypt init: %v", err)
}
_, err = io.WriteString(w, testMessageBenchmark)
if err != nil {
b.Fatalf("write: %v", err)
}
err = w.Close()
if err != nil {
b.Fatalf("encrypt close: %v", err)
}
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
if err != nil {
b.Fatalf("decrypt init: %v", err)
}
_, err = io.ReadAll(r)
if err != nil {
b.Fatalf("read: %v", err)
}
}
}
// TestConstants verifies the hardcoded constants
func TestConstants(t *testing.T) {
t.Parallel()
if purpose != 83696968 {
t.Errorf(
"purpose constant mismatch: expected 83696968, got %d",
purpose,
)
}
if vendorID != 592366788 {
t.Errorf(
"vendorID constant mismatch: expected 592366788, got %d",
vendorID,
)
}
if appID != 733482323 {
t.Errorf(
"appID constant mismatch: expected 733482323, got %d",
appID,
)
}
if hrp != "age-secret-key-" {
t.Errorf(
"hrp constant mismatch: expected 'age-secret-key-', got %q",
hrp,
)
}
}
// TestIdentityStringFormat tests that generated identities have the correct format
func TestIdentityStringFormat(t *testing.T) {
t.Parallel()
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive identity: %v", err)
}
secretKey := identity.String()
recipient := identity.Recipient().String()
// Check secret key format
if !strings.HasPrefix(secretKey, "AGE-SECRET-KEY-") {
t.Errorf(
"secret key should start with 'AGE-SECRET-KEY-', got: %s",
secretKey,
)
}
// Check recipient format
if !strings.HasPrefix(recipient, "age1") {
t.Errorf("recipient should start with 'age1', got: %s", recipient)
}
// Check that they're different
if secretKey == recipient {
t.Errorf("secret key and recipient should be different")
}
t.Logf("Secret key format: %s", secretKey)
t.Logf("Recipient format: %s", recipient)
}
// TestLargeMessageEncryption tests encryption/decryption of larger messages
func TestLargeMessageEncryption(t *testing.T) {
t.Parallel()
identity, err := DeriveIdentity(mnemonic, 0)
if err != nil {
t.Fatalf("derive identity: %v", err)
}
// Test with different message sizes
sizes := []int{1, 100, 1024, 10240, 100000}
for _, size := range sizes {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
t.Parallel()
message := strings.Repeat(testMessageLargePattern, size)
encryptDecryptRoundTrip(t, identity, message)
t.Logf("Successfully encrypted/decrypted %d byte message", size)
})
}
}
// encryptDecryptBytes encrypts data to id's recipient and returns the
// decrypted result.
func encryptDecryptBytes(t *testing.T, id *age.X25519Identity, data []byte) []byte {
t.Helper()
var ciphertext bytes.Buffer
encryptor, err := age.Encrypt(&ciphertext, id.Recipient())
if err != nil {
t.Fatalf("failed to create encryptor: %v", err)
}
_, err = encryptor.Write(data)
if err != nil {
t.Fatalf("failed to write data to encryptor: %v", err)
}
err = encryptor.Close()
if err != nil {
t.Fatalf("failed to close encryptor: %v", err)
}
decryptor, err := age.Decrypt(bytes.NewReader(ciphertext.Bytes()), id)
if err != nil {
t.Fatalf("failed to create decryptor: %v", err)
}
decrypted, err := io.ReadAll(decryptor)
if err != nil {
t.Fatalf("failed to read decrypted data: %v", err)
}
return decrypted
}
// requireIdenticalIdentities verifies that both identities have the same
// private and public keys.
func requireIdenticalIdentities(t *testing.T, id1, id2 *age.X25519Identity) {
t.Helper()
privateKey1 := id1.String()
privateKey2 := id2.String()
if privateKey1 != privateKey2 {
t.Fatalf(
"private keys should be identical:\nFirst: %s\nSecond: %s",
privateKey1,
privateKey2,
)
}
publicKey1 := id1.Recipient().String()
publicKey2 := id2.Recipient().String()
if publicKey1 != publicKey2 {
t.Fatalf(
"public keys should be identical:\nFirst: %s\nSecond: %s",
publicKey1,
publicKey2,
)
}
}
// TestRandomMnemonicDeterministicGeneration tests that:
// 1. A random mnemonic generates the same keys deterministically
// 2. Large data (1MB) can be encrypted and decrypted successfully
func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
t.Parallel()
// Generate a random mnemonic using the BIP39 library
entropy := make([]byte, 32) // 256 bits for 24-word mnemonic
_, err := rand.Read(entropy)
if err != nil {
t.Fatalf("failed to generate random entropy: %v", err)
}
randomMnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
t.Fatalf("failed to generate random mnemonic: %v", err)
}
t.Logf("Generated random mnemonic: %s", randomMnemonic)
// Test index for key derivation
testIndex := uint32(42)
// Generate the first identity
identity1, err := DeriveIdentity(randomMnemonic, testIndex)
if err != nil {
t.Fatalf("failed to derive first identity: %v", err)
}
// Generate the second identity with the same mnemonic and index
identity2, err := DeriveIdentity(randomMnemonic, testIndex)
if err != nil {
t.Fatalf("failed to derive second identity: %v", err)
}
// Verify that both identities have identical private and public keys
requireIdenticalIdentities(t, identity1, identity2)
t.Logf("Deterministic generation verified")
t.Logf("Private key: %s", identity1.String())
t.Logf("Public key: %s", identity1.Recipient().String())
// Generate 1 MB of random data for encryption test
testData := make([]byte, testDataSizeMegabyte)
_, err = rand.Read(testData)
if err != nil {
t.Fatalf("failed to generate random test data: %v", err)
}
t.Logf("Generated %d bytes of random test data", len(testData))
// Encrypt and decrypt the data with the first identity
decryptedData := encryptDecryptBytes(t, identity1, testData)
t.Logf("Decrypted %d bytes", len(decryptedData))
// Verify that the decrypted data matches the original
if len(decryptedData) != len(testData) {
t.Fatalf(
"decrypted data length mismatch: expected %d, got %d",
len(testData),
len(decryptedData),
)
}
if !bytes.Equal(testData, decryptedData) {
t.Fatalf("decrypted data does not match original data")
}
t.Logf("Large data encryption/decryption test passed successfully")
// Additional verification with the second identity (should work
// identically)
decryptedData2 := encryptDecryptBytes(t, identity2, testData)
if !bytes.Equal(testData, decryptedData2) {
t.Fatalf("second decrypted data does not match original data")
}
t.Logf("Cross-verification with second identity successful")
}