package vault_test import ( "path/filepath" "strings" "testing" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" ) //nolint:paralleltest // subtests share an in-memory filesystem sequentially func TestVaultMetadata(t *testing.T) { fs := afero.NewMemMapFs() t.Run("ComputeDoubleSHA256", func(t *testing.T) { testComputeDoubleSHA256(t) }) t.Run("GetNextDerivationIndex", func(t *testing.T) { testGetNextDerivationIndex(t, fs) }) t.Run("MetadataPersistence", func(t *testing.T) { testMetadataPersistence(t, fs) }) t.Run("DifferentKeysForDifferentIndices", func(t *testing.T) { testDifferentKeysForDifferentIndices(t) }) } func testComputeDoubleSHA256(t *testing.T) { t.Helper() // Test data data := []byte("test data") hash := vault.ComputeDoubleSHA256(data) // Verify it's a valid hex string of 64 characters (32 bytes * 2) if len(hash) != 64 { t.Errorf("Expected hash length of 64, got %d", len(hash)) } // Verify consistency hash2 := vault.ComputeDoubleSHA256(data) if hash != hash2 { t.Errorf("Hash should be consistent for same input") } // Verify different input produces different hash hash3 := vault.ComputeDoubleSHA256([]byte("different data")) if hash == hash3 { t.Errorf("Different input should produce different hash") } } // createVaultDirWithMetadata creates a vault directory containing a public // key derived from testMnemonic at the given index plus saved metadata, and // returns the derived public key hash. An empty familyHash defaults to the // derived key's own hash. func createVaultDirWithMetadata( t *testing.T, fs afero.Fs, vaultName string, derivationIndex uint32, familyHash string, ) string { t.Helper() vaultDir := filepath.Join(testStateDir, "vaults.d", vaultName) err := fs.MkdirAll(vaultDir, 0o700) if err != nil { t.Fatalf("Failed to create vault directory: %v", err) } // Derive identity for the requested index identity, err := agehd.DeriveIdentity(testMnemonic, derivationIndex) if err != nil { t.Fatalf("Failed to derive identity: %v", err) } pubKey := identity.Recipient().String() pubKeyHash := vault.ComputeDoubleSHA256([]byte(pubKey)) // Write public key err = afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(pubKey), 0o600) if err != nil { t.Fatalf("Failed to write public key: %v", err) } if familyHash == "" { familyHash = pubKeyHash } metadata := &vault.Metadata{ DerivationIndex: derivationIndex, PublicKeyHash: pubKeyHash, MnemonicFamilyHash: familyHash, } err = vault.SaveVaultMetadata(fs, vaultDir, metadata) if err != nil { t.Fatalf("Failed to save metadata: %v", err) } return pubKeyHash } func testGetNextDerivationIndex(t *testing.T, fs afero.Fs) { t.Helper() // Test with no existing vaults index, err := vault.GetNextDerivationIndex(fs, testStateDir, testMnemonic) if err != nil { t.Fatalf("Failed to get derivation index: %v", err) } if index != 0 { t.Errorf("Expected index 0 for first vault, got %d", index) } // Create a vault with metadata and matching public key (index 0; the // family hash is the index 0 key hash) pubKeyHash0 := createVaultDirWithMetadata(t, fs, "vault1", 0, "") // Next index for same mnemonic should be 1 index, err = vault.GetNextDerivationIndex(fs, testStateDir, testMnemonic) if err != nil { t.Fatalf("Failed to get derivation index: %v", err) } if index != 1 { t.Errorf("Expected index 1 for second vault with same mnemonic, got %d", index) } // Different mnemonic should start at 0 //nolint:dupword // BIP39-style test mnemonic differentMnemonic := "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" index, err = vault.GetNextDerivationIndex(fs, testStateDir, differentMnemonic) if err != nil { t.Fatalf("Failed to get derivation index: %v", err) } if index != 0 { t.Errorf("Expected index 0 for first vault with different mnemonic, got %d", index) } // Add another vault with same mnemonic but higher index (5), sharing // the same family hash since it's from the same mnemonic createVaultDirWithMetadata(t, fs, "vault2", 5, pubKeyHash0) // Next index should be 1 (not 6): we look for the first available slot index, err = vault.GetNextDerivationIndex(fs, testStateDir, testMnemonic) if err != nil { t.Fatalf("Failed to get derivation index: %v", err) } if index != 1 { t.Errorf("Expected index 1 (first available), got %d", index) } } func testMetadataPersistence(t *testing.T, fs afero.Fs) { t.Helper() vaultDir := filepath.Join(testStateDir, "vaults.d", testVaultName) err := fs.MkdirAll(vaultDir, 0o700) if err != nil { t.Fatalf("Failed to create vault directory: %v", err) } // Create and save metadata metadata := &vault.Metadata{ DerivationIndex: 3, PublicKeyHash: "test-public-key-hash", } err = vault.SaveVaultMetadata(fs, vaultDir, metadata) if err != nil { t.Fatalf("Failed to save metadata: %v", err) } // Load and verify loaded, err := vault.LoadVaultMetadata(fs, vaultDir) if err != nil { t.Fatalf("Failed to load metadata: %v", err) } if loaded.DerivationIndex != metadata.DerivationIndex { t.Errorf("DerivationIndex mismatch: expected %d, got %d", metadata.DerivationIndex, loaded.DerivationIndex) } if loaded.PublicKeyHash != metadata.PublicKeyHash { t.Errorf("PublicKeyHash mismatch: expected %s, got %s", metadata.PublicKeyHash, loaded.PublicKeyHash) } } func testDifferentKeysForDifferentIndices(t *testing.T) { t.Helper() // Derive keys with different indices identity0, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive identity with index 0: %v", err) } identity1, err := agehd.DeriveIdentity(testMnemonic, 1) if err != nil { t.Fatalf("Failed to derive identity with index 1: %v", err) } // Compute public key hashes pubKey0 := identity0.Recipient().String() pubKey1 := identity1.Recipient().String() hash0 := vault.ComputeDoubleSHA256([]byte(pubKey0)) // Verify different indices produce different public keys if pubKey0 == pubKey1 { t.Errorf("Different derivation indices should produce different public keys") } // But the hash of index 0's public key should be the same for the same // mnemonic. This is what we use as the identifier identity0Again, _ := agehd.DeriveIdentity(testMnemonic, 0) pubKey0Again := identity0Again.Recipient().String() hash0Again := vault.ComputeDoubleSHA256([]byte(pubKey0Again)) if hash0 != hash0Again { t.Errorf("Same mnemonic should produce same public key hash for index 0") } } func TestPublicKeyHashConsistency(t *testing.T) { t.Parallel() // Derive identity from index 0 multiple times identity1, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive first identity: %v", err) } identity2, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive second identity: %v", err) } // Verify identities are the same if identity1.Recipient().String() != identity2.Recipient().String() { t.Errorf("Identity derivation is not deterministic") t.Logf("First: %s", identity1.Recipient().String()) t.Logf("Second: %s", identity2.Recipient().String()) } // Compute public key hashes hash1 := vault.ComputeDoubleSHA256([]byte(identity1.Recipient().String())) hash2 := vault.ComputeDoubleSHA256([]byte(identity2.Recipient().String())) // Verify hashes are the same if hash1 != hash2 { t.Errorf("Public key hash computation is not deterministic") t.Logf("First hash: %s", hash1) t.Logf("Second hash: %s", hash2) } t.Logf("Test mnemonic public key hash (index 0): %s", hash1) } func TestSampleHashCalculation(t *testing.T) { t.Parallel() // Test with the exact mnemonic from integration test if available. We // also test with a few different mnemonics to make sure they produce // different hashes mnemonics := []string{ testMnemonic, "legal winner thank year wave sausage worth useful legal winner thank yellow", //nolint:dupword // BIP39-style test mnemonic "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", } for i, mnemonic := range mnemonics { identity, err := agehd.DeriveIdentity(mnemonic, 0) if err != nil { t.Fatalf("Failed to derive identity for mnemonic %d: %v", i, err) } hash := vault.ComputeDoubleSHA256([]byte(identity.Recipient().String())) t.Logf("Mnemonic %d hash (index 0): %s", i, hash) t.Logf(" Recipient: %s", identity.Recipient().String()) } } func TestWorkflowMismatch(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() fs := afero.NewOsFs() // Test Case 1: Create vault WITH mnemonic (like init command) t.Setenv("SB_SECRET_MNEMONIC", testMnemonic) _, err := vault.CreateVault(fs, tempDir, "default") if err != nil { t.Fatalf("Failed to create vault with mnemonic: %v", err) } // Load metadata for vault1 vault1Dir := filepath.Join(tempDir, "vaults.d", "default") metadata1, err := vault.LoadVaultMetadata(fs, vault1Dir) if err != nil { t.Fatalf("Failed to load vault1 metadata: %v", err) } t.Logf("Vault1 (with mnemonic) - DerivationIndex: %d, PublicKeyHash: %s", metadata1.DerivationIndex, metadata1.PublicKeyHash) // Test Case 2: Create vault WITHOUT mnemonic, then import (work vault) t.Setenv("SB_SECRET_MNEMONIC", "") _, err = vault.CreateVault(fs, tempDir, "work") if err != nil { t.Fatalf("Failed to create vault without mnemonic: %v", err) } vault2Dir := filepath.Join(tempDir, "vaults.d", "work") // Simulate the vault import process t.Setenv("SB_SECRET_MNEMONIC", testMnemonic) // Get the next available derivation index for this mnemonic derivationIndex, err := vault.GetNextDerivationIndex(fs, tempDir, testMnemonic) if err != nil { t.Fatalf("Failed to get next derivation index: %v", err) } t.Logf("Next derivation index for import: %d", derivationIndex) // Calculate public key hash from index 0 (same as in VaultImport) identity0, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive identity for index 0: %v", err) } publicKeyHash := vault.ComputeDoubleSHA256( []byte(identity0.Recipient().String())) // Load existing metadata and update it (same as in VaultImport) existingMetadata, err := vault.LoadVaultMetadata(fs, vault2Dir) if err != nil { t.Fatalf("Failed to load existing metadata: %v", err) } // Update metadata with new derivation info existingMetadata.DerivationIndex = derivationIndex existingMetadata.PublicKeyHash = publicKeyHash err = vault.SaveVaultMetadata(fs, vault2Dir, existingMetadata) if err != nil { t.Fatalf("Failed to save vault metadata: %v", err) } // Load updated metadata for vault2 metadata2, err := vault.LoadVaultMetadata(fs, vault2Dir) if err != nil { t.Fatalf("Failed to load vault2 metadata: %v", err) } t.Logf("Vault2 (imported mnemonic) - DerivationIndex: %d, PublicKeyHash: %s", metadata2.DerivationIndex, metadata2.PublicKeyHash) // Verify that both vaults have the same public key hash if metadata1.PublicKeyHash != metadata2.PublicKeyHash { t.Errorf("Public key hashes don't match!") t.Logf("Vault1 hash: %s", metadata1.PublicKeyHash) t.Logf("Vault2 hash: %s", metadata2.PublicKeyHash) } else { t.Logf("SUCCESS: Both vaults have the same public key hash: %s", metadata1.PublicKeyHash) } } func TestReverseEngineerHash(t *testing.T) { t.Parallel() // This is the hash that the work vault is getting in the failing test wrongHash := "e34a2f500e395d8934a90a99ee9311edcfffd68cb701079575e50cbac7bb9417" correctHash := "992552b00b3879dfae461fab9a084b47784a032771c7a9accaebdde05ec7a7d1" // Calculate hash for test mnemonic identity, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive identity: %v", err) } calculatedHash := vault.ComputeDoubleSHA256( []byte(identity.Recipient().String())) t.Logf("Test mnemonic hash: %s", calculatedHash) if calculatedHash == correctHash { t.Logf("Test mnemonic produces the correct hash") } else { t.Errorf("Test mnemonic does not produce the correct hash") } if calculatedHash == wrongHash { t.Logf("Test mnemonic unexpectedly produces the wrong hash") } // Try some other possibilities: maybe a string normalization issue? variations := []string{ testMnemonic, " " + testMnemonic + " ", testMnemonic + "\n", strings.TrimSpace(testMnemonic), } for i, variation := range variations { identity, err := agehd.DeriveIdentity(variation, 0) if err != nil { t.Logf("Variation %d failed: %v", i, err) continue } hash := vault.ComputeDoubleSHA256([]byte(identity.Recipient().String())) t.Logf("Variation %d hash: %s", i, hash) if hash == wrongHash { t.Logf("Found variation that produces wrong hash: '%s'", variation) } } // Maybe let's try an empty mnemonic or something else? emptyMnemonics := []string{ "", " ", } for i, emptyMnemonic := range emptyMnemonics { identity, err := agehd.DeriveIdentity(emptyMnemonic, 0) if err != nil { t.Logf("Empty mnemonic %d failed (expected): %v", i, err) continue } hash := vault.ComputeDoubleSHA256([]byte(identity.Recipient().String())) t.Logf("Empty mnemonic %d hash: %s", i, hash) if hash == wrongHash { t.Logf("Empty mnemonic produces wrong hash!") } } }