Update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 1m8s

- 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
This commit is contained in:
2026-08-07 17:27:23 +00:00
parent 6e5e0db999
commit 9ee216f629
59 changed files with 6568 additions and 4890 deletions

View File

@@ -1,208 +1,243 @@
package vault
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()
stateDir := "/test/state"
// Test mnemonic for consistent testing
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Run("ComputeDoubleSHA256", func(t *testing.T) {
// Test data
data := []byte("test data")
hash := 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 := ComputeDoubleSHA256(data)
if hash != hash2 {
t.Errorf("Hash should be consistent for same input")
}
// Verify different input produces different hash
hash3 := ComputeDoubleSHA256([]byte("different data"))
if hash == hash3 {
t.Errorf("Different input should produce different hash")
}
testComputeDoubleSHA256(t)
})
t.Run("GetNextDerivationIndex", func(t *testing.T) {
// Test with no existing vaults
index, err := GetNextDerivationIndex(fs, stateDir, 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
vaultDir := filepath.Join(stateDir, "vaults.d", "vault1")
if err := fs.MkdirAll(vaultDir, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err)
}
// Derive identity for index 0
identity0, err := agehd.DeriveIdentity(testMnemonic, 0)
if err != nil {
t.Fatalf("Failed to derive identity: %v", err)
}
pubKey0 := identity0.Recipient().String()
pubKeyHash0 := ComputeDoubleSHA256([]byte(pubKey0))
// Write public key
if err := afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(pubKey0), 0o600); err != nil {
t.Fatalf("Failed to write public key: %v", err)
}
metadata1 := &Metadata{
DerivationIndex: 0,
PublicKeyHash: pubKeyHash0, // Hash of the actual key (index 0)
MnemonicFamilyHash: pubKeyHash0, // Hash of index 0 key (for family identification)
}
if err := SaveVaultMetadata(fs, vaultDir, metadata1); err != nil {
t.Fatalf("Failed to save metadata: %v", err)
}
// Next index for same mnemonic should be 1
index, err = GetNextDerivationIndex(fs, stateDir, 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
differentMnemonic := "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
index, err = GetNextDerivationIndex(fs, stateDir, 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
vaultDir2 := filepath.Join(stateDir, "vaults.d", "vault2")
if err := fs.MkdirAll(vaultDir2, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err)
}
// Derive identity for index 5
identity5, err := agehd.DeriveIdentity(testMnemonic, 5)
if err != nil {
t.Fatalf("Failed to derive identity: %v", err)
}
pubKey5 := identity5.Recipient().String()
// Write public key
if err := afero.WriteFile(fs, filepath.Join(vaultDir2, "pub.age"), []byte(pubKey5), 0o600); err != nil {
t.Fatalf("Failed to write public key: %v", err)
}
// Compute the hash for index 5 key
pubKeyHash5 := ComputeDoubleSHA256([]byte(pubKey5))
metadata2 := &Metadata{
DerivationIndex: 5,
PublicKeyHash: pubKeyHash5, // Hash of the actual key (index 5)
MnemonicFamilyHash: pubKeyHash0, // Same family hash since it's from the same mnemonic
}
if err := SaveVaultMetadata(fs, vaultDir2, metadata2); err != nil {
t.Fatalf("Failed to save metadata: %v", err)
}
// Next index should be 1 (not 6) because we look for the first available slot
index, err = GetNextDerivationIndex(fs, stateDir, 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)
}
testGetNextDerivationIndex(t, fs)
})
t.Run("MetadataPersistence", func(t *testing.T) {
vaultDir := filepath.Join(stateDir, "vaults.d", "test-vault")
if err := fs.MkdirAll(vaultDir, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err)
}
// Create and save metadata
metadata := &Metadata{
DerivationIndex: 3,
PublicKeyHash: "test-public-key-hash",
}
if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
t.Fatalf("Failed to save metadata: %v", err)
}
// Load and verify
loaded, err := 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)
}
testMetadataPersistence(t, fs)
})
t.Run("DifferentKeysForDifferentIndices", func(t *testing.T) {
// 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 := 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 := ComputeDoubleSHA256([]byte(pubKey0Again))
if hash0 != hash0Again {
t.Errorf("Same mnemonic should produce same public key hash for index 0")
}
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) {
// Use the same test mnemonic that the integration test uses
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Parallel()
// Derive identity from index 0 multiple times
identity1, err := agehd.DeriveIdentity(testMnemonic, 0)
@@ -223,8 +258,8 @@ func TestPublicKeyHashConsistency(t *testing.T) {
}
// Compute public key hashes
hash1 := ComputeDoubleSHA256([]byte(identity1.Recipient().String()))
hash2 := ComputeDoubleSHA256([]byte(identity2.Recipient().String()))
hash1 := vault.ComputeDoubleSHA256([]byte(identity1.Recipient().String()))
hash2 := vault.ComputeDoubleSHA256([]byte(identity2.Recipient().String()))
// Verify hashes are the same
if hash1 != hash2 {
@@ -237,11 +272,15 @@ func TestPublicKeyHashConsistency(t *testing.T) {
}
func TestSampleHashCalculation(t *testing.T) {
// Test with the exact mnemonic from integration test if available
// We'll also test with a few different mnemonics to make sure they produce different hashes
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{
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
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",
}
@@ -251,29 +290,29 @@ func TestSampleHashCalculation(t *testing.T) {
t.Fatalf("Failed to derive identity for mnemonic %d: %v", i, err)
}
hash := ComputeDoubleSHA256([]byte(identity.Recipient().String()))
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) {
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// 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 := CreateVault(fs, tempDir, "default")
_, 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 := LoadVaultMetadata(fs, vault1Dir)
metadata1, err := vault.LoadVaultMetadata(fs, vault1Dir)
if err != nil {
t.Fatalf("Failed to load vault1 metadata: %v", err)
}
@@ -281,9 +320,10 @@ func TestWorkflowMismatch(t *testing.T) {
t.Logf("Vault1 (with mnemonic) - DerivationIndex: %d, PublicKeyHash: %s",
metadata1.DerivationIndex, metadata1.PublicKeyHash)
// Test Case 2: Create vault WITHOUT mnemonic, then import (like work vault)
// Test Case 2: Create vault WITHOUT mnemonic, then import (work vault)
t.Setenv("SB_SECRET_MNEMONIC", "")
_, err = CreateVault(fs, tempDir, "work")
_, err = vault.CreateVault(fs, tempDir, "work")
if err != nil {
t.Fatalf("Failed to create vault without mnemonic: %v", err)
}
@@ -294,7 +334,7 @@ func TestWorkflowMismatch(t *testing.T) {
t.Setenv("SB_SECRET_MNEMONIC", testMnemonic)
// Get the next available derivation index for this mnemonic
derivationIndex, err := GetNextDerivationIndex(fs, tempDir, testMnemonic)
derivationIndex, err := vault.GetNextDerivationIndex(fs, tempDir, testMnemonic)
if err != nil {
t.Fatalf("Failed to get next derivation index: %v", err)
}
@@ -306,10 +346,12 @@ func TestWorkflowMismatch(t *testing.T) {
if err != nil {
t.Fatalf("Failed to derive identity for index 0: %v", err)
}
publicKeyHash := ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
publicKeyHash := vault.ComputeDoubleSHA256(
[]byte(identity0.Recipient().String()))
// Load existing metadata and update it (same as in VaultImport)
existingMetadata, err := LoadVaultMetadata(fs, vault2Dir)
existingMetadata, err := vault.LoadVaultMetadata(fs, vault2Dir)
if err != nil {
t.Fatalf("Failed to load existing metadata: %v", err)
}
@@ -318,12 +360,13 @@ func TestWorkflowMismatch(t *testing.T) {
existingMetadata.DerivationIndex = derivationIndex
existingMetadata.PublicKeyHash = publicKeyHash
if err := SaveVaultMetadata(fs, vault2Dir, existingMetadata); err != nil {
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 := LoadVaultMetadata(fs, vault2Dir)
metadata2, err := vault.LoadVaultMetadata(fs, vault2Dir)
if err != nil {
t.Fatalf("Failed to load vault2 metadata: %v", err)
}
@@ -337,57 +380,59 @@ func TestWorkflowMismatch(t *testing.T) {
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)
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"
// Test mnemonic from integration test
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// Calculate hash for test mnemonic
identity, err := agehd.DeriveIdentity(testMnemonic, 0)
if err != nil {
t.Fatalf("Failed to derive identity: %v", err)
}
calculatedHash := ComputeDoubleSHA256([]byte(identity.Recipient().String()))
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")
t.Logf("Test mnemonic produces the correct hash")
} else {
t.Errorf("Test mnemonic does not produce the correct hash")
t.Errorf("Test mnemonic does not produce the correct hash")
}
if calculatedHash == wrongHash {
t.Logf("Test mnemonic unexpectedly produces the wrong hash")
t.Logf("Test mnemonic unexpectedly produces the wrong hash")
}
// Let's try some other possibilities - maybe there's a string normalization issue?
// Try some other possibilities: maybe a string normalization issue?
variations := []string{
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
" abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about ",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n",
strings.TrimSpace("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"),
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 := ComputeDoubleSHA256([]byte(identity.Recipient().String()))
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)
t.Logf("Found variation that produces wrong hash: '%s'", variation)
}
}
@@ -401,14 +446,15 @@ func TestReverseEngineerHash(t *testing.T) {
identity, err := agehd.DeriveIdentity(emptyMnemonic, 0)
if err != nil {
t.Logf("Empty mnemonic %d failed (expected): %v", i, err)
continue
}
hash := ComputeDoubleSHA256([]byte(identity.Recipient().String()))
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!")
t.Logf("Empty mnemonic produces wrong hash!")
}
}
}