check / check (push) Waiting to run
init and vault create put the mnemonic into the process environment for vault.CreateVault to read back, so every program they ran, gpg included, inherited it, and SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE were read at 13 places and never unset. Each command that may need them now reads both once, in its RunE, into locked buffers on the CLI Instance, and unsets them at once. The buffers are passed down: vault.CreateVault takes the mnemonic, a Vault carries Mnemonic and UnlockPassphrase, and the PGP, keychain and Secure Enclave unlocker constructors take both. Nothing below the command reads the environment. README warns against both variables. Model: opus-5-5
457 lines
13 KiB
Go
457 lines
13 KiB
Go
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) {
|
|
t.Parallel()
|
|
|
|
// Create a temporary directory for testing
|
|
tempDir := t.TempDir()
|
|
fs := afero.NewOsFs()
|
|
|
|
// Test Case 1: Create vault WITH mnemonic (like init command)
|
|
_, err := vault.CreateVault(fs, tempDir, "default", testMnemonicBuffer(t))
|
|
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)
|
|
_, err = vault.CreateVault(fs, tempDir, "work", nil)
|
|
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: 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!")
|
|
}
|
|
}
|
|
}
|