Compare commits

..

No commits in common. "main" and "fix/issue-14" have entirely different histories.

17 changed files with 14 additions and 372 deletions

View File

@ -71,8 +71,6 @@ func getUnlockerIDsCompletionFunc(fs afero.Fs, stateDir string) func(
unlockersDir := filepath.Join(vaultDir, "unlockers.d") unlockersDir := filepath.Join(vaultDir, "unlockers.d")
files, err := afero.ReadDir(fs, unlockersDir) files, err := afero.ReadDir(fs, unlockersDir)
if err != nil { if err != nil {
secret.Warn("Could not read unlockers directory during completion", "error", err)
continue continue
} }
@ -87,15 +85,11 @@ func getUnlockerIDsCompletionFunc(fs afero.Fs, stateDir string) func(
// Check if this is the right unlocker by comparing metadata // Check if this is the right unlocker by comparing metadata
metadataBytes, err := afero.ReadFile(fs, metadataPath) metadataBytes, err := afero.ReadFile(fs, metadataPath)
if err != nil { if err != nil {
secret.Warn("Could not read unlocker metadata during completion", "path", metadataPath, "error", err)
continue continue
} }
var diskMetadata secret.UnlockerMetadata var diskMetadata secret.UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil { if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
secret.Warn("Could not parse unlocker metadata during completion", "path", metadataPath, "error", err)
continue continue
} }

View File

@ -4,7 +4,6 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"git.eeqj.de/sneak/secret/internal/secret"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -29,8 +28,6 @@ func gatherVaultStats(
// Count secrets in this vault // Count secrets in this vault
secretEntries, err := afero.ReadDir(fs, secretsPath) secretEntries, err := afero.ReadDir(fs, secretsPath)
if err != nil { if err != nil {
secret.Warn("Could not read secrets directory for vault", "vault", vaultEntry.Name(), "error", err)
continue continue
} }
@ -46,8 +43,6 @@ func gatherVaultStats(
versionsPath := filepath.Join(secretPath, "versions") versionsPath := filepath.Join(secretPath, "versions")
versionEntries, err := afero.ReadDir(fs, versionsPath) versionEntries, err := afero.ReadDir(fs, versionsPath)
if err != nil { if err != nil {
secret.Warn("Could not read versions directory for secret", "secret", secretEntry.Name(), "error", err)
continue continue
} }

View File

@ -1047,6 +1047,7 @@ func test12SecretNameFormats(t *testing.T, tempDir, testMnemonic string, runSecr
// Test invalid secret names // Test invalid secret names
invalidNames := []string{ invalidNames := []string{
"", // empty "", // empty
"UPPERCASE", // uppercase not allowed
"with space", // spaces not allowed "with space", // spaces not allowed
"with@symbol", // special characters not allowed "with@symbol", // special characters not allowed
"with#hash", // special characters not allowed "with#hash", // special characters not allowed
@ -1072,7 +1073,7 @@ func test12SecretNameFormats(t *testing.T, tempDir, testMnemonic string, runSecr
// Some of these might not be invalid after all (e.g., leading/trailing slashes might be stripped, .hidden might be allowed) // Some of these might not be invalid after all (e.g., leading/trailing slashes might be stripped, .hidden might be allowed)
// For now, just check the ones we know should definitely fail // For now, just check the ones we know should definitely fail
definitelyInvalid := []string{"", "with space", "with@symbol", "with#hash", "with$dollar"} definitelyInvalid := []string{"", "UPPERCASE", "with space", "with@symbol", "with#hash", "with$dollar"}
shouldFail := false shouldFail := false
for _, invalid := range definitelyInvalid { for _, invalid := range definitelyInvalid {
if invalidName == invalid { if invalidName == invalid {

View File

@ -507,7 +507,7 @@ func (cli *Instance) ImportSecret(cmd *cobra.Command, secretName, sourceFile str
} }
defer func() { defer func() {
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
secret.Warn("Failed to close file", "error", err) secret.Debug("Failed to close file", "error", err)
} }
}() }()

View File

@ -271,8 +271,6 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
// Create unlocker instance to get the proper ID // Create unlocker instance to get the proper ID
vaultDir, err := vlt.GetDirectory() vaultDir, err := vlt.GetDirectory()
if err != nil { if err != nil {
secret.Warn("Could not get vault directory while listing unlockers", "error", err)
continue continue
} }
@ -280,8 +278,6 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
unlockersDir := filepath.Join(vaultDir, "unlockers.d") unlockersDir := filepath.Join(vaultDir, "unlockers.d")
files, err := afero.ReadDir(cli.fs, unlockersDir) files, err := afero.ReadDir(cli.fs, unlockersDir)
if err != nil { if err != nil {
secret.Warn("Could not read unlockers directory", "error", err)
continue continue
} }
@ -297,16 +293,12 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
// Check if this is the right unlocker by comparing metadata // Check if this is the right unlocker by comparing metadata
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath) metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
if err != nil { if err != nil {
secret.Warn("Could not read unlocker metadata file", "path", metadataPath, "error", err) continue // FIXME this error needs to be handled
continue
} }
var diskMetadata secret.UnlockerMetadata var diskMetadata secret.UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil { if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
secret.Warn("Could not parse unlocker metadata file", "path", metadataPath, "error", err) continue // FIXME this error needs to be handled
continue
} }
// Match by type and creation time // Match by type and creation time
@ -332,7 +324,6 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
} else { } else {
// Generate ID as fallback // Generate ID as fallback
properID = fmt.Sprintf("%s-%s", metadata.CreatedAt.Format("2006-01-02.15.04"), metadata.Type) properID = fmt.Sprintf("%s-%s", metadata.CreatedAt.Format("2006-01-02.15.04"), metadata.Type)
secret.Warn("Could not create unlocker instance, using fallback ID", "fallback_id", properID, "type", metadata.Type)
} }
unlockerInfo := UnlockerInfo{ unlockerInfo := UnlockerInfo{
@ -599,16 +590,12 @@ func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) er
// Get the list of unlockers and check if any match the ID // Get the list of unlockers and check if any match the ID
unlockers, err := vlt.ListUnlockers() unlockers, err := vlt.ListUnlockers()
if err != nil { if err != nil {
secret.Warn("Could not list unlockers during duplicate check", "error", err)
return nil // If we can't list unlockers, assume it doesn't exist return nil // If we can't list unlockers, assume it doesn't exist
} }
// Get vault directory to construct unlocker instances // Get vault directory to construct unlocker instances
vaultDir, err := vlt.GetDirectory() vaultDir, err := vlt.GetDirectory()
if err != nil { if err != nil {
secret.Warn("Could not get vault directory during duplicate check", "error", err)
return nil return nil
} }
@ -618,8 +605,6 @@ func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) er
unlockersDir := filepath.Join(vaultDir, "unlockers.d") unlockersDir := filepath.Join(vaultDir, "unlockers.d")
files, err := afero.ReadDir(cli.fs, unlockersDir) files, err := afero.ReadDir(cli.fs, unlockersDir)
if err != nil { if err != nil {
secret.Warn("Could not read unlockers directory during duplicate check", "error", err)
continue continue
} }
@ -634,15 +619,11 @@ func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) er
// Check if this matches our metadata // Check if this matches our metadata
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath) metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
if err != nil { if err != nil {
secret.Warn("Could not read unlocker metadata during duplicate check", "path", metadataPath, "error", err)
continue continue
} }
var diskMetadata secret.UnlockerMetadata var diskMetadata secret.UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil { if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
secret.Warn("Could not parse unlocker metadata during duplicate check", "path", metadataPath, "error", err)
continue continue
} }

View File

@ -164,7 +164,7 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
// Load metadata // Load metadata
if err := sv.LoadMetadata(ltIdentity); err != nil { if err := sv.LoadMetadata(ltIdentity); err != nil {
secret.Warn("Failed to load version metadata", "version", version, "error", err) secret.Debug("Failed to load version metadata", "version", version, "error", err)
// Display version with error // Display version with error
status := "error" status := "error"
if version == currentVersion { if version == currentVersion {

View File

@ -58,16 +58,6 @@ func IsDebugEnabled() bool {
return debugEnabled return debugEnabled
} }
// Warn logs a warning message to stderr unconditionally (visible without --verbose or debug flags)
func Warn(msg string, args ...any) {
output := fmt.Sprintf("WARNING: %s", msg)
for i := 0; i+1 < len(args); i += 2 {
output += fmt.Sprintf(" %s=%v", args[i], args[i+1])
}
output += "\n"
fmt.Fprint(os.Stderr, output)
}
// Debug logs a debug message with optional attributes // Debug logs a debug message with optional attributes
func Debug(msg string, args ...any) { func Debug(msg string, args ...any) {
if !debugEnabled { if !debugEnabled {

View File

@ -1,82 +0,0 @@
package secret
import (
"encoding/json"
"path/filepath"
"testing"
"time"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// realVault is a minimal VaultInterface backed by a real afero filesystem,
// using the same directory layout as vault.Vault.
type realVault struct {
name string
stateDir string
fs afero.Fs
}
func (v *realVault) GetDirectory() (string, error) {
return filepath.Join(v.stateDir, "vaults.d", v.name), nil
}
func (v *realVault) GetName() string { return v.name }
func (v *realVault) GetFilesystem() afero.Fs { return v.fs }
// Unused by getLongTermPrivateKey — these satisfy VaultInterface.
func (v *realVault) AddSecret(string, *memguard.LockedBuffer, bool) error { panic("not used") }
func (v *realVault) GetCurrentUnlocker() (Unlocker, error) { panic("not used") }
func (v *realVault) CreatePassphraseUnlocker(*memguard.LockedBuffer) (*PassphraseUnlocker, error) {
panic("not used")
}
// createRealVault sets up a complete vault directory structure on an in-memory
// filesystem, identical to what vault.CreateVault produces.
func createRealVault(t *testing.T, fs afero.Fs, stateDir, name string, derivationIndex uint32) *realVault {
t.Helper()
vaultDir := filepath.Join(stateDir, "vaults.d", name)
require.NoError(t, fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), DirPerms))
require.NoError(t, fs.MkdirAll(filepath.Join(vaultDir, "unlockers.d"), DirPerms))
metadata := VaultMetadata{
CreatedAt: time.Now(),
DerivationIndex: derivationIndex,
}
metaBytes, err := json.Marshal(metadata)
require.NoError(t, err)
require.NoError(t, afero.WriteFile(fs, filepath.Join(vaultDir, "vault-metadata.json"), metaBytes, FilePerms))
return &realVault{name: name, stateDir: stateDir, fs: fs}
}
func TestGetLongTermPrivateKeyUsesVaultDerivationIndex(t *testing.T) {
const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// Derive expected keys at two different indices to prove they differ.
key0, err := agehd.DeriveIdentity(testMnemonic, 0)
require.NoError(t, err)
key5, err := agehd.DeriveIdentity(testMnemonic, 5)
require.NoError(t, err)
require.NotEqual(t, key0.String(), key5.String(),
"sanity check: different derivation indices must produce different keys")
// Build a real vault with DerivationIndex=5 on an in-memory filesystem.
fs := afero.NewMemMapFs()
vault := createRealVault(t, fs, "/state", "test-vault", 5)
t.Setenv(EnvMnemonic, testMnemonic)
result, err := getLongTermPrivateKey(fs, vault)
require.NoError(t, err)
defer result.Destroy()
assert.Equal(t, key5.String(), string(result.Bytes()),
"getLongTermPrivateKey should derive at vault's DerivationIndex (5)")
assert.NotEqual(t, key0.String(), string(result.Bytes()),
"getLongTermPrivateKey must not use hardcoded index 0")
}

View File

@ -53,10 +53,7 @@ func DetermineStateDir(customConfigDir string) (string, error) {
return "", fmt.Errorf("unable to determine state directory: config dir: %w, home dir: %w", err, homeErr) return "", fmt.Errorf("unable to determine state directory: config dir: %w, home dir: %w", err, homeErr)
} }
fallbackDir := filepath.Join(homeDir, ".config", AppID) return filepath.Join(homeDir, ".config", AppID), nil
Warn("Could not determine user config directory, falling back to default", "fallback", fallbackDir, "error", err)
return fallbackDir, nil
} }
return filepath.Join(configDir, AppID), nil return filepath.Join(configDir, AppID), nil

View File

@ -251,25 +251,8 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedB
// Check if mnemonic is available in environment variable // Check if mnemonic is available in environment variable
envMnemonic := os.Getenv(EnvMnemonic) envMnemonic := os.Getenv(EnvMnemonic)
if envMnemonic != "" { if envMnemonic != "" {
// Read vault metadata to get the correct derivation index // Use mnemonic directly to derive long-term key
vaultDir, err := vault.GetDirectory() ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to get vault directory: %w", err)
}
metadataPath := filepath.Join(vaultDir, "vault-metadata.json")
metadataBytes, err := afero.ReadFile(fs, metadataPath)
if err != nil {
return nil, fmt.Errorf("failed to read vault metadata: %w", err)
}
var metadata VaultMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return nil, fmt.Errorf("failed to parse vault metadata: %w", err)
}
// Use mnemonic with the vault's actual derivation index
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, metadata.DerivationIndex)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
} }

View File

@ -257,10 +257,9 @@ func isValidSecretName(name string) bool {
if name == "" { if name == "" {
return false return false
} }
// Valid characters for secret names: letters, numbers, dash, dot, underscore, slash // Valid characters for secret names: lowercase letters, numbers, dash, dot, underscore, slash
for _, char := range name { for _, char := range name {
if (char < 'a' || char > 'z') && // lowercase letters if (char < 'a' || char > 'z') && // lowercase letters
(char < 'A' || char > 'Z') && // uppercase letters
(char < '0' || char > '9') && // numbers (char < '0' || char > '9') && // numbers
char != '-' && // dash char != '-' && // dash
char != '.' && // dot char != '.' && // dot
@ -284,9 +283,7 @@ func TestSecretNameValidation(t *testing.T) {
{"valid/path/name", true}, {"valid/path/name", true},
{"123valid", true}, {"123valid", true},
{"", false}, {"", false},
{"Valid-Upper-Name", true}, // uppercase allowed {"Invalid-Name", false}, // uppercase not allowed
{"2025-11-21-ber1app1-vaultik-test-bucket-AKI", true}, // real-world uppercase key ID
{"MixedCase/Path/Name", true}, // mixed case with path
{"invalid name", false}, // space not allowed {"invalid name", false}, // space not allowed
{"invalid@name", false}, // @ not allowed {"invalid@name", false}, // @ not allowed
} }

View File

@ -102,8 +102,6 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
var serial int var serial int
if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err != nil { if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err != nil {
Warn("Skipping malformed version directory name", "name", entry.Name(), "error", err)
continue continue
} }

View File

@ -1,96 +0,0 @@
package vault
import (
"testing"
"git.eeqj.de/sneak/secret/internal/secret"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestGetSecretVersionRejectsPathTraversal verifies that GetSecretVersion
// validates the secret name and rejects path traversal attempts.
// This is a regression test for https://git.eeqj.de/sneak/secret/issues/13
func TestGetSecretVersionRejectsPathTraversal(t *testing.T) {
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Setenv(secret.EnvMnemonic, testMnemonic)
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
fs := afero.NewMemMapFs()
stateDir := "/test/state"
vlt, err := CreateVault(fs, stateDir, "test-vault")
require.NoError(t, err)
// Add a legitimate secret so the vault is set up
value := memguard.NewBufferFromBytes([]byte("legitimate-secret"))
err = vlt.AddSecret("legit", value, false)
require.NoError(t, err)
// These names contain path traversal and should be rejected
maliciousNames := []string{
"../../../etc/passwd",
"..%2f..%2fetc/passwd",
".secret",
"../sibling-vault/secrets.d/target",
"foo/../bar",
"a/../../etc/passwd",
}
for _, name := range maliciousNames {
t.Run(name, func(t *testing.T) {
_, err := vlt.GetSecretVersion(name, "")
assert.Error(t, err, "GetSecretVersion should reject malicious name: %s", name)
assert.Contains(t, err.Error(), "invalid secret name",
"error should indicate invalid name for: %s", name)
})
}
}
// TestGetSecretRejectsPathTraversal verifies GetSecret (which calls GetSecretVersion)
// also rejects path traversal names.
func TestGetSecretRejectsPathTraversal(t *testing.T) {
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Setenv(secret.EnvMnemonic, testMnemonic)
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
fs := afero.NewMemMapFs()
stateDir := "/test/state"
vlt, err := CreateVault(fs, stateDir, "test-vault")
require.NoError(t, err)
_, err = vlt.GetSecret("../../../etc/passwd")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid secret name")
}
// TestGetSecretObjectRejectsPathTraversal verifies GetSecretObject
// also validates names and rejects path traversal attempts.
func TestGetSecretObjectRejectsPathTraversal(t *testing.T) {
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Setenv(secret.EnvMnemonic, testMnemonic)
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
fs := afero.NewMemMapFs()
stateDir := "/test/state"
vlt, err := CreateVault(fs, stateDir, "test-vault")
require.NoError(t, err)
maliciousNames := []string{
"../../../etc/passwd",
"foo/../bar",
"a/../../etc/passwd",
}
for _, name := range maliciousNames {
t.Run(name, func(t *testing.T) {
_, err := vlt.GetSecretObject(name)
assert.Error(t, err, "GetSecretObject should reject: %s", name)
assert.Contains(t, err.Error(), "invalid secret name")
})
}
}

View File

@ -67,7 +67,7 @@ func (v *Vault) ListSecrets() ([]string, error) {
return secrets, nil return secrets, nil
} }
// isValidSecretName validates secret names according to the format [a-zA-Z0-9\.\-\_\/]+ // isValidSecretName validates secret names according to the format [a-z0-9\.\-\_\/]+
// but with additional restrictions: // but with additional restrictions:
// - No leading or trailing slashes // - No leading or trailing slashes
// - No double slashes // - No double slashes
@ -92,15 +92,8 @@ func isValidSecretName(name string) bool {
return false return false
} }
// Check for path traversal via ".." components
for _, part := range strings.Split(name, "/") {
if part == ".." {
return false
}
}
// Check the basic pattern // Check the basic pattern
matched, _ := regexp.MatchString(`^[a-zA-Z0-9\.\-\_\/]+$`, name) matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_\/]+$`, name)
return matched return matched
} }
@ -326,13 +319,6 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
slog.String("version", version), slog.String("version", version),
) )
// Validate secret name to prevent path traversal
if !isValidSecretName(name) {
secret.Debug("Invalid secret name provided", "secret_name", name)
return nil, fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name)
}
// Get vault directory // Get vault directory
vaultDir, err := v.GetDirectory() vaultDir, err := v.GetDirectory()
if err != nil { if err != nil {
@ -468,10 +454,6 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
// GetSecretObject retrieves a Secret object with metadata loaded from this vault // GetSecretObject retrieves a Secret object with metadata loaded from this vault
func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) { func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
if !isValidSecretName(name) {
return nil, fmt.Errorf("invalid secret name: %s", name)
}
// First check if the secret exists by checking for the metadata file // First check if the secret exists by checking for the metadata file
vaultDir, err := v.GetDirectory() vaultDir, err := v.GetDirectory()
if err != nil { if err != nil {

View File

@ -1,42 +0,0 @@
package vault
import "testing"
func TestIsValidSecretNameUppercase(t *testing.T) {
tests := []struct {
name string
valid bool
}{
// Lowercase (existing behavior)
{"valid-name", true},
{"valid.name", true},
{"valid_name", true},
{"valid/path/name", true},
{"123valid", true},
// Uppercase (new behavior - issue #2)
{"Valid-Upper-Name", true},
{"2025-11-21-ber1app1-vaultik-test-bucket-AKI", true},
{"MixedCase/Path/Name", true},
{"ALLUPPERCASE", true},
{"ABC123", true},
// Still invalid
{"", false},
{"invalid name", false},
{"invalid@name", false},
{".dotstart", false},
{"/leading-slash", false},
{"trailing-slash/", false},
{"double//slash", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isValidSecretName(tt.name)
if result != tt.valid {
t.Errorf("isValidSecretName(%q) = %v, want %v", tt.name, result, tt.valid)
}
})
}
}

View File

@ -213,9 +213,7 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err) return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
} }
if !exists { if !exists {
secret.Warn("Skipping unlocker directory with missing metadata file", "directory", file.Name()) return nil, fmt.Errorf("unlocker directory %s is missing metadata file", file.Name())
continue
} }
metadataBytes, err := afero.ReadFile(v.fs, metadataPath) metadataBytes, err := afero.ReadFile(v.fs, metadataPath)

View File

@ -243,57 +243,3 @@ func TestVaultOperations(t *testing.T) {
} }
}) })
} }
func TestListUnlockers_SkipsMissingMetadata(t *testing.T) {
// Set test environment variables
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
t.Setenv(secret.EnvMnemonic, testMnemonic)
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
// Use in-memory filesystem
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault
vlt, err := CreateVault(fs, stateDir, "test-vault")
if err != nil {
t.Fatalf("Failed to create vault: %v", err)
}
// Create a passphrase unlocker so we have at least one valid unlocker
passphraseBuffer := memguard.NewBufferFromBytes([]byte("test-passphrase"))
defer passphraseBuffer.Destroy()
_, err = vlt.CreatePassphraseUnlocker(passphraseBuffer)
if err != nil {
t.Fatalf("Failed to create passphrase unlocker: %v", err)
}
// Create a bogus unlocker directory with no metadata file
vaultDir, err := vlt.GetDirectory()
if err != nil {
t.Fatalf("Failed to get vault directory: %v", err)
}
bogusDir := filepath.Join(vaultDir, "unlockers.d", "bogus-no-metadata")
err = fs.MkdirAll(bogusDir, 0o700)
if err != nil {
t.Fatalf("Failed to create bogus directory: %v", err)
}
// ListUnlockers should succeed, skipping the bogus directory
unlockers, err := vlt.ListUnlockers()
if err != nil {
t.Fatalf("ListUnlockers returned error when it should have skipped bad directory: %v", err)
}
// Should still have the valid passphrase unlocker
if len(unlockers) == 0 {
t.Errorf("Expected at least one unlocker, got none")
}
// Verify we only got the valid unlocker(s), not the bogus one
for _, u := range unlockers {
if u.Type == "" {
t.Errorf("Got unlocker with empty type, likely from bogus directory")
}
}
}