Files
secret/pkg/agehd/agehd_test.go
clawbot 41cea400a7
All checks were successful
check / check (push) Successful in 43s
Update golangci-lint to v2.12.2 with canonical config (#29)
Bumps golangci-lint from v2.1.6 (digest-only pin in the `Dockerfile` lint stage) to v2.12.2, pinned by tag and digest (Debian-based image).

Replaces `.golangci.yml` with the canonical strict config: all linters enabled except the standard disable list (`exhaustruct`, `depguard`, `godot`, `wsl`, `wrapcheck`, `varnamelen`), `lll` at 88, `funlen` 80/50, `cyclop` 15, `dupl` 100, and test files are now linted (the old config had `tests: false`, an enable-only list of ~20 linters, `lll` 120, and a blanket exclusion of `internal/macse`).

The stricter config surfaced ~1550 findings, all fixed:

- `wsl_v5` (439) / `nlreturn` (24): blank-line insertions
- `lll` (309): line wrapping at 88 columns; long literals split with `+` concatenation, values unchanged
- `noinlineerr` (130): `if err := ...` split into assignment plus check
- `paralleltest` (116): `t.Parallel()` added to tests without shared state; reasoned `//nolint` where `t.Setenv` or shared fixtures forbid it
- `err113` (97): package-level sentinel errors (new `internal/vault/errors.go`), `%w` wrapping, `errors.Is`
- `perfsprint` (74) / `modernize` (39) / `intrange`: `strconv`, `errors.New`, `slices.Contains`, `any`, `SplitSeq`
- `goconst` (40) / `dupword` (41) / `testifylint` (42) / `thelper` (33): constants, assertion fixes, `t.Helper()`
- `noctx` (22): `exec.CommandContext` for gpg/CLI invocations
- `testpackage` (18): black-box tests moved to `_test` packages where they use only exported identifiers; white-box files carry a reasoned `//nolint`
- `funlen`/`cyclop`/`gocognit`/`nestif`/`dupl`: behavior-preserving helper extraction
- assorted singletons: `gosec`, `gosmopolitan`, `funcorder`, `nonamedreturns`, `makezero`, `prealloc`, `godox`, `nolintlint`, `ireturn`, `nilnil`, `gochecknoinits`

## User-visible strings

**None changed.** Every error message this branch composes is byte-identical to the one `main` composes.

The `err113` sentinels are shaped so `fmt.Errorf` reassembles the original text around them: the sentinel carries the fixed words and the caller supplies the interpolated value in the position it has always occupied. Where the value sits mid-sentence the sentinel holds only a fragment (e.g. `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, not by inspection: every `fmt.Errorf` and `errors.New` call site in both trees is parsed, the `Error()` text of any sentinel passed to `%w` is substituted in, and the resulting sets of composed message templates are compared. All 350 templates `main` produces are still produced, character for character. The set of lost or altered messages is empty.

## `unlocker list`

`findUnlockerIDByMetadata` returns `(string, error)` rather than signalling failure with an empty ID, so an unreadable `unlockers.d` is no longer indistinguishable from "no matching entry". `UnlockersList` skips such an entry with a warning naming the directory — its behavior before the scan was extracted into a helper — instead of 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 `internal/cli/unlockers_list_test.go`.

`TODO.md` records the change plus follow-ups (version-completion TODOs formerly in code comments, darwin-gated files exceeding 88 columns that Linux CI does not lint).

`make check` is green and the pinned v2.12.2 image reports `0 issues.` Note the test suite needs the memlock ulimit from `script/cibuild` for the 10MB memguard test; that requirement is pre-existing.

Not changed: `script/bootstrap` installs golangci-lint via the system package manager (no version pin to bump), and `script/lint` invokes whatever `golangci-lint` is on PATH. golangci-lint v2.12 deprecates `gomodguard` in favor of `gomodguard_v2` (warning only); the canonical config owns that decision.

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #29
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-10 15:23:33 +02: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")
}