passes tests now!

This commit is contained in:
Jeffrey Paul 2025-06-20 07:24:48 -07:00
parent 0b31fba663
commit 004dce5472
19 changed files with 165 additions and 756 deletions

View File

@ -9,6 +9,7 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/term"
)
@ -19,6 +20,7 @@ var stdinScanner *bufio.Scanner
type CLIInstance struct {
fs afero.Fs
stateDir string
cmd *cobra.Command
}
// NewCLIInstance creates a new CLI instance with the real filesystem

View File

@ -22,6 +22,7 @@ func newEncryptCmd() *cobra.Command {
outputFile, _ := cmd.Flags().GetString("output")
cli := NewCLIInstance()
cli.cmd = cmd
return cli.Encrypt(args[0], inputFile, outputFile)
},
}
@ -42,6 +43,7 @@ func newDecryptCmd() *cobra.Command {
outputFile, _ := cmd.Flags().GetString("output")
cli := NewCLIInstance()
cli.cmd = cmd
return cli.Decrypt(args[0], inputFile, outputFile)
},
}
@ -127,7 +129,7 @@ func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error
}
// Set up output writer
var output io.Writer = os.Stdout
var output io.Writer = cli.cmd.OutOrStdout()
if outputFile != "" {
file, err := cli.fs.Create(outputFile)
if err != nil {
@ -213,7 +215,7 @@ func (cli *CLIInstance) Decrypt(secretName, inputFile, outputFile string) error
}
// Set up output writer
var output io.Writer = os.Stdout
var output io.Writer = cli.cmd.OutOrStdout()
if outputFile != "" {
file, err := cli.fs.Create(outputFile)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
"time"
"git.eeqj.de/sneak/secret/internal/cli"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -50,10 +51,13 @@ func TestMain(m *testing.M) {
// all functionality of the secret manager using a real filesystem in a temporary directory.
// This test serves as both validation and documentation of the program's behavior.
func TestSecretManagerIntegration(t *testing.T) {
// Enable debug logging to diagnose test failures
// Enable debug logging to diagnose issues
os.Setenv("GODEBUG", "berlin.sneak.pkg.secret")
defer os.Unsetenv("GODEBUG")
// Reinitialize debug logging to pick up the environment variable change
secret.InitDebugLogging()
// Test configuration
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
testPassphrase := "test-passphrase-123"
@ -349,10 +353,18 @@ func test01Initialize(t *testing.T, tempDir, testMnemonic, testPassphrase string
currentUnlockerFile := filepath.Join(defaultVaultDir, "current-unlocker")
verifyFileExists(t, currentUnlockerFile)
// Read the current-unlocker file to see what it contains
// Read the current-unlocker symlink to see what it points to
symlinkTarget, err := os.Readlink(currentUnlockerFile)
if err != nil {
t.Logf("DEBUG: failed to read symlink %s: %v", currentUnlockerFile, err)
// Fallback to reading as file if it's not a symlink
currentUnlockerContent := readFile(t, currentUnlockerFile)
// The file likely contains the unlocker ID
t.Logf("DEBUG: current-unlocker file content: %q", string(currentUnlockerContent))
assert.Contains(t, string(currentUnlockerContent), "passphrase", "current unlocker should be passphrase type")
} else {
t.Logf("DEBUG: current-unlocker symlink points to: %q", symlinkTarget)
assert.Contains(t, symlinkTarget, "passphrase", "current unlocker should be passphrase type")
}
// Verify vault-metadata.json in vault
vaultMetadata := filepath.Join(defaultVaultDir, "vault-metadata.json")
@ -1006,6 +1018,7 @@ func test13UnlockerManagement(t *testing.T, tempDir, testMnemonic string, runSec
// List unlockers
output, err := runSecret("unlockers", "list")
require.NoError(t, err, "unlockers list should succeed")
t.Logf("DEBUG: unlockers list output: %q", output)
// Should have the passphrase unlocker created during init
assert.Contains(t, output, "passphrase", "should have passphrase unlocker")
@ -1034,6 +1047,7 @@ func test13UnlockerManagement(t *testing.T, tempDir, testMnemonic string, runSec
}
// Note: This might still show 1 if the implementation doesn't support multiple passphrase unlockers
// Just verify we have at least 1
t.Logf("DEBUG: passphrase count: %d, output lines: %v", passphraseCount, lines)
assert.GreaterOrEqual(t, passphraseCount, 1, "should have at least 1 passphrase unlocker")
// Test JSON output
@ -1309,6 +1323,7 @@ func test18AgeKeyOperations(t *testing.T, tempDir, secretPath, testMnemonic stri
"SB_SECRET_MNEMONIC": testMnemonic,
}, "encrypt", "encryption/key", "--input", testFile)
require.NoError(t, err, "encrypt to stdout should succeed")
t.Logf("DEBUG: encrypt output: %q", output)
assert.Contains(t, output, "age-encryption.org", "should output age format")
// Test that the age key was stored as a secret
@ -1804,10 +1819,10 @@ func test28VaultMetadata(t *testing.T, tempDir string) {
require.NoError(t, err, "default vault metadata should be valid JSON")
// Verify required fields
assert.Equal(t, "default", defaultMetadata["name"])
assert.Equal(t, float64(0), defaultMetadata["derivation_index"])
assert.Contains(t, defaultMetadata, "createdAt")
assert.Contains(t, defaultMetadata, "public_key_hash")
assert.Contains(t, defaultMetadata, "mnemonic_family_hash")
// Check work vault metadata
workMetadataPath := filepath.Join(tempDir, "vaults.d", "work", "vault-metadata.json")
@ -1819,13 +1834,12 @@ func test28VaultMetadata(t *testing.T, tempDir string) {
require.NoError(t, err, "work vault metadata should be valid JSON")
// Work vault should have different derivation index
assert.Equal(t, "work", workMetadata["name"])
workIndex := workMetadata["derivation_index"].(float64)
assert.NotEqual(t, float64(0), workIndex, "work vault should have non-zero derivation index")
// Both vaults created with same mnemonic should have same public_key_hash
assert.Equal(t, defaultMetadata["public_key_hash"], workMetadata["public_key_hash"],
"vaults from same mnemonic should have same public_key_hash")
// Both vaults created with same mnemonic should have same mnemonic_family_hash
assert.Equal(t, defaultMetadata["mnemonic_family_hash"], workMetadata["mnemonic_family_hash"],
"vaults from same mnemonic should have same mnemonic_family_hash")
}
func test29SymlinkHandling(t *testing.T, tempDir, secretPath, testMnemonic string) {
@ -2025,7 +2039,7 @@ func test31EnvMnemonicUsesVaultDerivationIndex(t *testing.T, tempDir, secretPath
// This is the expected behavior with the current bug
assert.Error(t, err, "get should fail due to wrong derivation index")
assert.Contains(t, getOutput, "failed to decrypt", "should indicate decryption failure")
assert.Contains(t, getOutput, "derived public key does not match vault", "should indicate key derivation failure")
// Document what should happen when the bug is fixed
t.Log("When the bug is fixed, GetValue should read vault metadata and use derivation index 1")

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"git.eeqj.de/sneak/secret/internal/secret"
@ -25,6 +24,7 @@ func newAddCmd() *cobra.Command {
secret.Debug("Got force flag", "force", force)
cli := NewCLIInstance()
cli.cmd = cmd // Set the command for stdin access
secret.Debug("Created CLI instance, calling AddSecret")
return cli.AddSecret(args[0], force)
},
@ -110,7 +110,7 @@ func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
// Read secret value from stdin
secret.Debug("Reading secret value from stdin")
value, err := io.ReadAll(os.Stdin)
value, err := io.ReadAll(cli.cmd.InOrStdin())
if err != nil {
return fmt.Errorf("failed to read secret value: %w", err)
}
@ -167,6 +167,15 @@ func (cli *CLIInstance) GetSecretWithVersion(cmd *cobra.Command, secretName stri
// Print the secret value to stdout
cmd.Print(string(value))
secret.Debug("Printed value to cmd")
// Debug: Log what we're actually printing
secret.Debug("Secret retrieval debug info",
"secretName", secretName,
"version", version,
"valueLength", len(value),
"valueAsString", string(value),
"isEmpty", len(value) == 0)
return nil
}

View File

@ -45,6 +45,11 @@ func ExecuteCommandInProcess(args []string, stdin string, env map[string]string)
output := buf.String()
secret.Debug("Command execution completed", "error", err, "outputLength", len(output), "output", output)
// Add debug info for troubleshooting
if len(output) == 0 && err == nil {
secret.Debug("Warning: Command executed successfully but produced no output", "args", args)
}
// Restore environment
for k, v := range savedEnv {
if v == "" {

View File

@ -40,6 +40,7 @@ func newUnlockersListCmd() *cobra.Command {
jsonOutput, _ := cmd.Flags().GetBool("json")
cli := NewCLIInstance()
cli.cmd = cmd
return cli.UnlockersList(jsonOutput)
},
}
@ -201,31 +202,31 @@ func (cli *CLIInstance) UnlockersList(jsonOutput bool) error {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(jsonBytes))
cli.cmd.Println(string(jsonBytes))
} else {
// Pretty table output
if len(unlockers) == 0 {
fmt.Println("No unlockers found in current vault.")
fmt.Println("Run 'secret unlockers add passphrase' to create one.")
cli.cmd.Println("No unlockers found in current vault.")
cli.cmd.Println("Run 'secret unlockers add passphrase' to create one.")
return nil
}
fmt.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
fmt.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----")
cli.cmd.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
cli.cmd.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----")
for _, unlocker := range unlockers {
flags := ""
if len(unlocker.Flags) > 0 {
flags = strings.Join(unlocker.Flags, ",")
}
fmt.Printf("%-18s %-12s %-20s %s\n",
cli.cmd.Printf("%-18s %-12s %-20s %s\n",
unlocker.ID,
unlocker.Type,
unlocker.CreatedAt.Format("2006-01-02 15:04:05"),
flags)
}
fmt.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
cli.cmd.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
}
return nil

View File

@ -223,13 +223,17 @@ func (cli *CLIInstance) VaultImport(cmd *cobra.Command, vaultName string) error
return fmt.Errorf("failed to store long-term public key: %w", err)
}
// Calculate public key hash from index 0 (same for all vaults with this mnemonic)
// Calculate public key hash from the actual derivation index being used
// This is used to verify that the derived key matches what was stored
publicKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.Recipient().String()))
// Calculate family hash from index 0 (same for all vaults with this mnemonic)
// This is used to identify which vaults belong to the same mnemonic family
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
if err != nil {
return fmt.Errorf("failed to derive identity for index 0: %w", err)
}
publicKeyHash := vault.ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
familyHash := vault.ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
// Load existing metadata
existingMetadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
@ -243,6 +247,7 @@ func (cli *CLIInstance) VaultImport(cmd *cobra.Command, vaultName string) error
// Update metadata with new derivation info
existingMetadata.DerivationIndex = derivationIndex
existingMetadata.PublicKeyHash = publicKeyHash
existingMetadata.MnemonicFamilyHash = familyHash
if err := vault.SaveVaultMetadata(cli.fs, vaultDir, existingMetadata); err != nil {
secret.Debug("Failed to save vault metadata", "error", err)

View File

@ -2,7 +2,6 @@ package cli
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
@ -110,7 +109,7 @@ func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) erro
}
// Create table writer
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER")
// Load and display each version's metadata
@ -185,15 +184,8 @@ func (cli *CLIInstance) PromoteVersion(cmd *cobra.Command, secretName string, ve
return fmt.Errorf("version '%s' not found for secret '%s'", version, secretName)
}
// Update the current symlink
currentLink := filepath.Join(secretDir, "current")
// Remove existing symlink
_ = cli.fs.Remove(currentLink)
// Create new symlink to the selected version
relativePath := filepath.Join("versions", version)
if err := afero.WriteFile(cli.fs, currentLink, []byte(relativePath), 0644); err != nil {
// Update the current symlink using the proper function
if err := secret.SetCurrentVersion(cli.fs, secretDir, version); err != nil {
return fmt.Errorf("failed to update current version: %w", err)
}

View File

@ -18,11 +18,11 @@ var (
)
func init() {
initDebugLogging()
InitDebugLogging()
}
// initDebugLogging initializes the debug logging system based on GODEBUG environment variable
func initDebugLogging() {
// InitDebugLogging initializes the debug logging system based on current GODEBUG environment variable
func InitDebugLogging() {
godebug := os.Getenv("GODEBUG")
debugEnabled = strings.Contains(godebug, "berlin.sneak.pkg.secret")

View File

@ -21,7 +21,7 @@ func TestDebugLogging(t *testing.T) {
os.Setenv("GODEBUG", originalGodebug)
}
// Re-initialize debug system with original setting
initDebugLogging()
InitDebugLogging()
}()
tests := []struct {
@ -61,7 +61,7 @@ func TestDebugLogging(t *testing.T) {
}
// Re-initialize debug system
initDebugLogging()
InitDebugLogging()
// Test if debug is enabled
enabled := IsDebugEnabled()
@ -112,10 +112,10 @@ func TestDebugFunctions(t *testing.T) {
} else {
os.Setenv("GODEBUG", originalGodebug)
}
initDebugLogging()
InitDebugLogging()
}()
initDebugLogging()
InitDebugLogging()
if !IsDebugEnabled() {
t.Log("Debug not enabled, but continuing with debug function tests anyway")

View File

@ -9,7 +9,8 @@ type VaultMetadata struct {
CreatedAt time.Time `json:"createdAt"`
Description string `json:"description,omitempty"`
DerivationIndex uint32 `json:"derivation_index"`
PublicKeyHash string `json:"public_key_hash,omitempty"` // Double SHA256 hash of the long-term public key
PublicKeyHash string `json:"public_key_hash,omitempty"` // Double SHA256 hash of the actual long-term public key
MnemonicFamilyHash string `json:"mnemonic_family_hash,omitempty"` // Double SHA256 hash of index-0 key (for grouping vaults from same mnemonic)
}
// UnlockerMetadata contains information about an unlocker

View File

@ -287,22 +287,33 @@ func (sv *SecretVersion) GetValue(ltIdentity *age.X25519Identity) ([]byte, error
slog.String("version", sv.Version),
)
// Debug: Log the directory and long-term key info
Debug("SecretVersion GetValue debug info",
"secret_name", sv.SecretName,
"version", sv.Version,
"directory", sv.Directory,
"lt_identity_public_key", ltIdentity.Recipient().String())
fs := sv.vault.GetFilesystem()
// Step 1: Read encrypted version private key
encryptedPrivKeyPath := filepath.Join(sv.Directory, "priv.age")
Debug("Reading encrypted version private key", "path", encryptedPrivKeyPath)
encryptedPrivKey, err := afero.ReadFile(fs, encryptedPrivKeyPath)
if err != nil {
Debug("Failed to read encrypted version private key", "error", err, "path", encryptedPrivKeyPath)
return nil, fmt.Errorf("failed to read encrypted version private key: %w", err)
}
Debug("Successfully read encrypted version private key", "path", encryptedPrivKeyPath, "size", len(encryptedPrivKey))
// Step 2: Decrypt version private key using long-term key
Debug("Decrypting version private key with long-term identity", "version", sv.Version)
versionPrivKeyData, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
if err != nil {
Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
return nil, fmt.Errorf("failed to decrypt version private key: %w", err)
}
Debug("Successfully decrypted version private key", "version", sv.Version, "size", len(versionPrivKeyData))
// Step 3: Parse version private key
versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
@ -313,20 +324,27 @@ func (sv *SecretVersion) GetValue(ltIdentity *age.X25519Identity) ([]byte, error
// Step 4: Read encrypted value
encryptedValuePath := filepath.Join(sv.Directory, "value.age")
Debug("Reading encrypted value", "path", encryptedValuePath)
encryptedValue, err := afero.ReadFile(fs, encryptedValuePath)
if err != nil {
Debug("Failed to read encrypted version value", "error", err, "path", encryptedValuePath)
return nil, fmt.Errorf("failed to read encrypted version value: %w", err)
}
Debug("Successfully read encrypted value", "path", encryptedValuePath, "size", len(encryptedValue))
// Step 5: Decrypt value using version key
Debug("Decrypting value with version identity", "version", sv.Version)
value, err := DecryptWithIdentity(encryptedValue, versionIdentity)
if err != nil {
Debug("Failed to decrypt version value", "error", err, "version", sv.Version)
return nil, fmt.Errorf("failed to decrypt version value: %w", err)
}
Debug("Successfully retrieved version value", "version", sv.Version, "value_length", len(value))
Debug("Successfully retrieved version value",
"version", sv.Version,
"value_length", len(value),
"value_as_string", string(value),
"is_empty", len(value) == 0)
return value, nil
}

View File

@ -207,6 +207,7 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
mnemonic := os.Getenv(secret.EnvMnemonic)
var derivationIndex uint32
var publicKeyHash string
var familyHash string
if mnemonic != "" {
secret.Debug("Mnemonic found in environment, deriving long-term key", "vault", name)
@ -232,13 +233,16 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
}
secret.Debug("Wrote long-term public key", "path", ltPubKeyPath)
// Compute public key hash from index 0 (same for all vaults with this mnemonic)
// Compute verification hash from actual derivation index
publicKeyHash = ComputeDoubleSHA256([]byte(ltIdentity.Recipient().String()))
// Compute family hash from index 0 (same for all vaults with this mnemonic)
// This is used to identify which vaults belong to the same mnemonic family
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to derive identity for index 0: %w", err)
}
publicKeyHash = ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
familyHash = ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
} else {
secret.Debug("No mnemonic in environment, vault created without long-term key", "vault", name)
// Use 0 for derivation index when no mnemonic is provided
@ -250,6 +254,7 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
CreatedAt: time.Now(),
DerivationIndex: derivationIndex,
PublicKeyHash: publicKeyHash,
MnemonicFamilyHash: familyHash,
}
if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
return nil, fmt.Errorf("failed to save vault metadata: %w", err)

View File

@ -75,8 +75,8 @@ func GetNextDerivationIndex(fs afero.Fs, stateDir string, mnemonic string) (uint
continue
}
// Check if this vault uses the same mnemonic by comparing public key hashes
if metadata.PublicKeyHash == pubKeyHash {
// Check if this vault uses the same mnemonic by comparing family hashes
if metadata.MnemonicFamilyHash == pubKeyHash {
usedIndices[metadata.DerivationIndex] = true
}
}

View File

@ -72,7 +72,8 @@ func TestVaultMetadata(t *testing.T) {
metadata1 := &VaultMetadata{
DerivationIndex: 0,
PublicKeyHash: pubKeyHash0,
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)
@ -115,9 +116,13 @@ func TestVaultMetadata(t *testing.T) {
t.Fatalf("Failed to write public key: %v", err)
}
// Compute the hash for index 5 key
pubKeyHash5 := ComputeDoubleSHA256([]byte(pubKey5))
metadata2 := &VaultMetadata{
DerivationIndex: 5,
PublicKeyHash: pubKeyHash0, // Same hash since it's from the same mnemonic
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)

View File

@ -351,6 +351,7 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
)
// Get the version's value
secret.Debug("About to call secretVersion.GetValue", "version", version, "secret_name", name)
decryptedValue, err := secretVersion.GetValue(longTermIdentity)
if err != nil {
secret.Debug("Failed to decrypt version value", "error", err, "version", version, "secret_name", name)
@ -364,6 +365,13 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
slog.Int("decrypted_length", len(decryptedValue)),
)
// Debug: Log metadata about the decrypted value without exposing the actual secret
secret.Debug("Vault secret decryption debug info",
"secret_name", name,
"version", version,
"decrypted_value_length", len(decryptedValue),
"is_empty", len(decryptedValue) == 0)
return decryptedValue, nil
}

View File

@ -34,17 +34,23 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
// Resolve the symlink to get the target directory
var unlockerDir string
if _, ok := v.fs.(*afero.OsFs); ok {
secret.Debug("Resolving unlocker symlink (real filesystem)")
// For real filesystems, resolve the symlink properly
unlockerDir, err = ResolveVaultSymlink(v.fs, currentUnlockerPath)
if linkReader, ok := v.fs.(afero.LinkReader); ok {
secret.Debug("Resolving unlocker symlink using afero")
// Try to read as symlink first
unlockerDir, err = linkReader.ReadlinkIfPossible(currentUnlockerPath)
if err != nil {
secret.Debug("Failed to resolve unlocker symlink", "error", err, "symlink_path", currentUnlockerPath)
return nil, fmt.Errorf("failed to resolve current unlocker symlink: %w", err)
secret.Debug("Failed to read symlink, falling back to file contents", "error", err, "symlink_path", currentUnlockerPath)
// Fallback: read the path from file contents
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
if err != nil {
secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
}
unlockerDir = strings.TrimSpace(string(unlockerDirBytes))
}
} else {
secret.Debug("Reading unlocker path (mock filesystem)")
// Fallback for mock filesystems: read the path from file contents
secret.Debug("Reading unlocker path (filesystem doesn't support symlinks)")
// Fallback for filesystems that don't support symlinks: read the path from file contents
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
if err != nil {
secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
@ -319,8 +325,21 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
}
}
// Create new symlink
return afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms)
// Create new symlink using afero's SymlinkIfPossible
if linker, ok := v.fs.(afero.Linker); ok {
secret.Debug("Creating unlocker symlink", "target", targetUnlockerDir, "link", currentUnlockerPath)
if err := linker.SymlinkIfPossible(targetUnlockerDir, currentUnlockerPath); err != nil {
return fmt.Errorf("failed to create unlocker symlink: %w", err)
}
} else {
// Fallback: create a regular file with the target path for filesystems that don't support symlinks
secret.Debug("Fallback: creating regular file with target path", "target", targetUnlockerDir)
if err := afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms); err != nil {
return fmt.Errorf("failed to create unlocker symlink file: %w", err)
}
}
return nil
}
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker

View File

@ -84,6 +84,17 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
// Verify that the derived key matches the stored public key hash
derivedPubKeyHash := ComputeDoubleSHA256([]byte(ltIdentity.Recipient().String()))
if derivedPubKeyHash != metadata.PublicKeyHash {
secret.Debug("Derived public key hash does not match stored hash",
"vault_name", v.Name,
"derived_hash", derivedPubKeyHash,
"stored_hash", metadata.PublicKeyHash,
"derivation_index", metadata.DerivationIndex)
return nil, fmt.Errorf("derived public key does not match vault: mnemonic may be incorrect")
}
secret.DebugWith("Successfully derived long-term key from mnemonic",
slog.String("vault_name", v.Name),
slog.String("public_key", ltIdentity.Recipient().String()),

View File

@ -1,688 +0,0 @@
#!/bin/bash
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test configuration
TEST_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
TEST_PASSPHRASE="test-passphrase-123"
TEMP_DIR="$(mktemp -d)"
SECRET_BINARY="./secret"
# Enable debug output from the secret program
export GODEBUG="berlin.sneak.pkg.secret"
echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
echo -e "${YELLOW}Debug output enabled: GODEBUG=$GODEBUG${NC}"
echo -e "${YELLOW}Note: All tests use environment variables (no manual input)${NC}"
# Function to print test steps
print_step() {
echo -e "\n${BLUE}Step $1: $2${NC}"
}
# Function to print success
print_success() {
echo -e "${GREEN}$1${NC}"
}
# Function to print error and exit
print_error() {
echo -e "${RED}$1${NC}"
exit 1
}
# Function to print warning (for expected failures)
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
# Function to clear state directory and reset environment
reset_state() {
echo -e "${YELLOW}Resetting state directory...${NC}"
# Safety checks before removing anything
if [ -z "$TEMP_DIR" ]; then
print_error "TEMP_DIR is not set, cannot reset state safely"
fi
if [ ! -d "$TEMP_DIR" ]; then
print_error "TEMP_DIR ($TEMP_DIR) is not a directory, cannot reset state safely"
fi
# Additional safety: ensure TEMP_DIR looks like a temp directory
case "$TEMP_DIR" in
/tmp/* | /var/folders/* | */tmp/*)
# Looks like a reasonable temp directory path
;;
*)
print_error "TEMP_DIR ($TEMP_DIR) does not look like a safe temporary directory path"
;;
esac
# Now it's safe to remove contents - use find to avoid glob expansion issues
find "${TEMP_DIR:?}" -mindepth 1 -delete 2>/dev/null || true
unset SB_SECRET_MNEMONIC
unset SB_UNLOCK_PASSPHRASE
export SB_SECRET_STATE_DIR="$TEMP_DIR"
}
# Cleanup function
cleanup() {
echo -e "\n${YELLOW}Cleaning up...${NC}"
rm -rf "$TEMP_DIR"
unset SB_SECRET_STATE_DIR
unset SB_SECRET_MNEMONIC
unset SB_UNLOCK_PASSPHRASE
unset GODEBUG
echo -e "${GREEN}Cleanup complete${NC}"
}
# Set cleanup trap
trap cleanup EXIT
# Check that the secret binary exists
if [ ! -f "$SECRET_BINARY" ]; then
print_error "Secret binary not found at $SECRET_BINARY. Please run 'make build' first."
fi
# Test 1: Set up environment variables
print_step "1" "Setting up environment variables"
export SB_SECRET_STATE_DIR="$TEMP_DIR"
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
print_success "Environment variables set"
echo " SB_SECRET_STATE_DIR=$SB_SECRET_STATE_DIR"
echo " SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
# Test 2: Initialize the secret manager (should create default vault)
print_step "2" "Initializing secret manager (creates default vault)"
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
echo " SB_UNLOCK_PASSPHRASE=$SB_UNLOCK_PASSPHRASE"
# Verify environment variables are exported and visible to subprocesses
echo "Verifying environment variables are exported:"
env | grep -E "^SB_" || true
echo "Running: $SECRET_BINARY init"
# Run with explicit environment to ensure variables are passed
if SB_SECRET_STATE_DIR="$SB_SECRET_STATE_DIR" \
SB_SECRET_MNEMONIC="$SB_SECRET_MNEMONIC" \
SB_UNLOCK_PASSPHRASE="$SB_UNLOCK_PASSPHRASE" \
GODEBUG="$GODEBUG" \
$SECRET_BINARY init </dev/null; then
print_success "Secret manager initialized with default vault"
else
print_error "Failed to initialize secret manager"
fi
unset SB_UNLOCK_PASSPHRASE
# Verify directory structure was created
if [ -d "$TEMP_DIR" ]; then
print_success "State directory created: $TEMP_DIR"
else
print_error "State directory was not created"
fi
# Test 3: Vault management
print_step "3" "Testing vault management"
# List vaults (should show default)
echo "Listing vaults..."
echo "Running: $SECRET_BINARY vault list"
if $SECRET_BINARY vault list; then
VAULTS=$($SECRET_BINARY vault list)
echo "Available vaults: $VAULTS"
print_success "Listed vaults successfully"
else
print_error "Failed to list vaults"
fi
# Create a new vault
echo "Creating new vault 'work'..."
echo "Running: $SECRET_BINARY vault create work"
if $SECRET_BINARY vault create work; then
print_success "Created vault 'work'"
else
print_error "Failed to create vault 'work'"
fi
# Create another vault
echo "Creating new vault 'personal'..."
echo "Running: $SECRET_BINARY vault create personal"
if $SECRET_BINARY vault create personal; then
print_success "Created vault 'personal'"
else
print_error "Failed to create vault 'personal'"
fi
# List vaults again (should show default, work, personal)
echo "Listing vaults after creation..."
echo "Running: $SECRET_BINARY vault list"
if $SECRET_BINARY vault list; then
VAULTS=$($SECRET_BINARY vault list)
echo "Available vaults: $VAULTS"
print_success "Listed vaults after creation"
else
print_error "Failed to list vaults after creation"
fi
# Switch to work vault
echo "Switching to 'work' vault..."
echo "Running: $SECRET_BINARY vault select work"
if $SECRET_BINARY vault select work; then
print_success "Switched to 'work' vault"
else
print_error "Failed to switch to 'work' vault"
fi
# Test 4: Import functionality with environment variable combinations
print_step "4" "Testing import functionality with environment variable combinations"
# Test 4a: Import with both env vars set (typical usage)
echo -e "\n${YELLOW}Test 4a: Import with both SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set${NC}"
reset_state
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
# Create a vault first
echo "Running: $SECRET_BINARY vault create test-vault"
if $SECRET_BINARY vault create test-vault; then
print_success "Created test-vault for import testing"
else
print_error "Failed to create test-vault"
fi
# Import should work without prompts
echo "Importing with both env vars set (automated)..."
echo "Running: $SECRET_BINARY vault import test-vault"
if $SECRET_BINARY vault import test-vault; then
print_success "Import succeeded with both env vars (automated)"
else
print_error "Import failed with both env vars"
fi
# Test 4b: Import into non-existent vault (should fail)
echo -e "\n${YELLOW}Test 4b: Import into non-existent vault (should fail)${NC}"
echo "Importing into non-existent vault (should fail)..."
if $SECRET_BINARY vault import nonexistent-vault; then
print_error "Import should have failed for non-existent vault"
else
print_success "Import correctly failed for non-existent vault"
fi
# Test 4c: Import with invalid mnemonic (should fail)
echo -e "\n${YELLOW}Test 4c: Import with invalid mnemonic (should fail)${NC}"
export SB_SECRET_MNEMONIC="invalid mnemonic phrase that should not work"
# Create a vault first
echo "Running: $SECRET_BINARY vault create test-vault2"
if $SECRET_BINARY vault create test-vault2; then
print_success "Created test-vault2 for invalid mnemonic testing"
else
print_error "Failed to create test-vault2"
fi
echo "Importing with invalid mnemonic (should fail)..."
if $SECRET_BINARY vault import test-vault2; then
print_error "Import should have failed with invalid mnemonic"
else
print_success "Import correctly failed with invalid mnemonic"
fi
# Reset state for remaining tests
reset_state
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
# Test 5: Unlocker management
print_step "5" "Testing unlocker management"
# Initialize with mnemonic and passphrase
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
echo "Running: $SECRET_BINARY init (with SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set)"
if $SECRET_BINARY init; then
print_success "Initialized for unlocker testing"
else
print_error "Failed to initialize for unlocker testing"
fi
# Create passphrase-protected unlocker
echo "Creating passphrase-protected unlocker..."
echo "Running: $SECRET_BINARY unlockers add passphrase (with SB_UNLOCK_PASSPHRASE set)"
if $SECRET_BINARY unlockers add passphrase; then
print_success "Created passphrase-protected unlocker"
else
print_error "Failed to create passphrase-protected unlocker"
exit 1
fi
unset SB_UNLOCK_PASSPHRASE
# List unlockers
echo "Listing unlockers..."
echo "Running: $SECRET_BINARY unlockers list"
if $SECRET_BINARY unlockers list; then
UNLOCKERS=$($SECRET_BINARY unlockers list)
echo "Available unlockers: $UNLOCKERS"
print_success "Listed unlockers"
else
print_error "Failed to list unlockers"
exit 1
fi
# Test 6: Secret management with mnemonic (keyless operation)
print_step "6" "Testing mnemonic-based secret operations (keyless)"
# Add secrets using mnemonic (no unlocker required)
echo "Adding secrets using mnemonic-based long-term key..."
# Test secret 1
echo "Running: echo \"my-super-secret-password\" | $SECRET_BINARY add \"database/password\""
if echo "my-super-secret-password" | $SECRET_BINARY add "database/password"; then
print_success "Added secret: database/password"
else
print_error "Failed to add secret: database/password"
fi
# Test secret 2
echo "Running: echo \"api-key-12345\" | $SECRET_BINARY add \"api/key\""
if echo "api-key-12345" | $SECRET_BINARY add "api/key"; then
print_success "Added secret: api/key"
else
print_error "Failed to add secret: api/key"
fi
# Test secret 3 (with path)
echo "Running: echo \"ssh-private-key-content\" | $SECRET_BINARY add \"ssh/private-key\""
if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key"; then
print_success "Added secret: ssh/private-key"
else
print_error "Failed to add secret: ssh/private-key"
fi
# Test secret 4 (with dots and underscores)
echo "Running: echo \"jwt-secret-token\" | $SECRET_BINARY add \"app.config_jwt_secret\""
if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret"; then
print_success "Added secret: app.config_jwt_secret"
else
print_error "Failed to add secret: app.config_jwt_secret"
fi
# Retrieve secrets using mnemonic
echo "Retrieving secrets using mnemonic-based long-term key..."
# Retrieve and verify secret 1
RETRIEVED_SECRET1=$($SECRET_BINARY get "database/password" 2>/dev/null)
if [ "$RETRIEVED_SECRET1" = "my-super-secret-password" ]; then
print_success "Retrieved and verified secret: database/password"
else
print_error "Failed to retrieve or verify secret: database/password"
fi
# Retrieve and verify secret 2
RETRIEVED_SECRET2=$($SECRET_BINARY get "api/key" 2>/dev/null)
if [ "$RETRIEVED_SECRET2" = "api-key-12345" ]; then
print_success "Retrieved and verified secret: api/key"
else
print_error "Failed to retrieve or verify secret: api/key"
fi
# Retrieve and verify secret 3
RETRIEVED_SECRET3=$($SECRET_BINARY get "ssh/private-key" 2>/dev/null)
if [ "$RETRIEVED_SECRET3" = "ssh-private-key-content" ]; then
print_success "Retrieved and verified secret: ssh/private-key"
else
print_error "Failed to retrieve or verify secret: ssh/private-key"
fi
# List all secrets
echo "Listing all secrets..."
echo "Running: $SECRET_BINARY list"
if $SECRET_BINARY list; then
SECRETS=$($SECRET_BINARY list)
echo "Secrets in current vault:"
echo "$SECRETS" | while read -r secret; do
echo " - $secret"
done
print_success "Listed all secrets"
else
print_error "Failed to list secrets"
fi
# Test 7: Secret management without mnemonic (traditional unlocker approach)
print_step "7" "Testing traditional unlocker approach"
# Create a new vault without mnemonic
echo "Running: $SECRET_BINARY vault create traditional"
$SECRET_BINARY vault create traditional
# Add a secret using traditional unlocker approach
echo "Adding secret using traditional unlocker..."
echo "Running: echo 'traditional-secret' | $SECRET_BINARY add traditional/secret"
if echo "traditional-secret" | $SECRET_BINARY add traditional/secret; then
print_success "Added secret with traditional approach"
else
print_error "Failed to add secret with traditional approach"
fi
# Retrieve secret using traditional unlocker approach
echo "Retrieving secret using traditional unlocker approach..."
echo "Running: $SECRET_BINARY get traditional/secret"
if RETRIEVED=$($SECRET_BINARY get traditional/secret 2>&1); then
print_success "Retrieved: $RETRIEVED"
else
print_error "Failed to retrieve secret with traditional approach"
fi
# Test 8: Advanced unlocker management
print_step "8" "Testing advanced unlocker management"
if [ "$PLATFORM" = "darwin" ]; then
# macOS only: Test Secure Enclave
echo "Testing Secure Enclave unlocker creation..."
if $SECRET_BINARY unlockers add sep; then
print_success "Created Secure Enclave unlocker"
else
print_warning "Secure Enclave unlocker creation not yet implemented"
fi
fi
# Get current unlocker ID for testing
echo "Getting current unlocker for testing..."
echo "Running: $SECRET_BINARY unlockers list"
if $SECRET_BINARY unlockers list; then
CURRENT_UNLOCKER_ID=$($SECRET_BINARY unlockers list | head -n1 | awk '{print $1}')
if [ -n "$CURRENT_UNLOCKER_ID" ]; then
print_success "Found unlocker ID: $CURRENT_UNLOCKER_ID"
# Test unlocker selection
echo "Testing unlocker selection..."
echo "Running: $SECRET_BINARY unlocker select $CURRENT_UNLOCKER_ID"
if $SECRET_BINARY unlocker select "$CURRENT_UNLOCKER_ID"; then
print_success "Selected unlocker: $CURRENT_UNLOCKER_ID"
else
print_warning "Unlocker selection not yet implemented"
fi
fi
fi
# Test 9: Secret name validation and edge cases
print_step "9" "Testing secret name validation and edge cases"
# Test valid names
VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
for name in "${VALID_NAMES[@]}"; do
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name --force"
if echo "test-value" | $SECRET_BINARY add "$name" --force; then
print_success "Valid name accepted: $name"
else
print_error "Valid name rejected: $name"
fi
done
# Test invalid names (these should fail)
echo "Testing invalid names (should fail)..."
INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
for name in "${INVALID_NAMES[@]}"; do
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name"
if echo "test-value" | $SECRET_BINARY add "$name"; then
print_error "Invalid name accepted (should have been rejected): '$name'"
else
print_success "Invalid name correctly rejected: '$name'"
fi
done
# Test 10: Overwrite protection and force flag
print_step "10" "Testing overwrite protection and force flag"
# Try to add existing secret without --force (should fail)
echo "Running: echo \"new-value\" | $SECRET_BINARY add \"database/password\""
if echo "new-value" | $SECRET_BINARY add "database/password"; then
print_error "Overwrite protection failed - secret was overwritten without --force"
else
print_success "Overwrite protection working - secret not overwritten without --force"
fi
# Try to add existing secret with --force (should succeed)
echo "Running: echo \"new-password-value\" | $SECRET_BINARY add \"database/password\" --force"
if echo "new-password-value" | $SECRET_BINARY add "database/password" --force; then
print_success "Force overwrite working - secret overwritten with --force"
# Verify the new value
RETRIEVED_NEW=$($SECRET_BINARY get "database/password" 2>/dev/null)
if [ "$RETRIEVED_NEW" = "new-password-value" ]; then
print_success "Overwritten secret has correct new value"
else
print_error "Overwritten secret has incorrect value"
fi
else
print_error "Force overwrite failed - secret not overwritten with --force"
fi
# Test 11: Cross-vault operations
print_step "11" "Testing cross-vault operations"
# First create and import mnemonic into work vault since it was destroyed by reset_state
echo "Creating work vault for cross-vault testing..."
echo "Running: $SECRET_BINARY vault create work"
if $SECRET_BINARY vault create work; then
print_success "Created work vault for cross-vault testing"
else
print_error "Failed to create work vault for cross-vault testing"
fi
# Import mnemonic into work vault so it can store secrets
echo "Importing mnemonic into work vault..."
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
echo "Running: $SECRET_BINARY vault import work"
if $SECRET_BINARY vault import work; then
print_success "Imported mnemonic into work vault"
else
print_error "Failed to import mnemonic into work vault"
fi
unset SB_UNLOCK_PASSPHRASE
# Switch to work vault and add secrets there
echo "Switching to 'work' vault for cross-vault testing..."
echo "Running: $SECRET_BINARY vault select work"
if $SECRET_BINARY vault select work; then
print_success "Switched to 'work' vault"
# Add work-specific secrets
echo "Running: echo \"work-database-password\" | $SECRET_BINARY add \"work/database\""
if echo "work-database-password" | $SECRET_BINARY add "work/database"; then
print_success "Added work-specific secret"
else
print_error "Failed to add work-specific secret"
fi
# List secrets in work vault
echo "Running: $SECRET_BINARY list"
if $SECRET_BINARY list; then
WORK_SECRETS=$($SECRET_BINARY list)
echo "Secrets in work vault: $WORK_SECRETS"
print_success "Listed work vault secrets"
else
print_error "Failed to list work vault secrets"
fi
else
print_error "Failed to switch to 'work' vault"
fi
# Switch back to default vault
echo "Switching back to 'default' vault..."
echo "Running: $SECRET_BINARY vault select default"
if $SECRET_BINARY vault select default; then
print_success "Switched back to 'default' vault"
# Verify default vault secrets are still there
echo "Running: $SECRET_BINARY get \"database/password\""
if $SECRET_BINARY get "database/password"; then
print_success "Default vault secrets still accessible"
else
print_error "Default vault secrets not accessible"
fi
else
print_error "Failed to switch back to 'default' vault"
fi
# Test 12: File structure verification
print_step "12" "Verifying file structure"
echo "Checking file structure in $TEMP_DIR..."
if [ -d "$TEMP_DIR/vaults.d/default/secrets.d" ]; then
print_success "Default vault structure exists"
# Check a specific secret's file structure
SECRET_DIR="$TEMP_DIR/vaults.d/default/secrets.d/database%password"
if [ -d "$SECRET_DIR" ]; then
print_success "Secret directory exists: database%password"
# Check required files for per-secret key architecture
FILES=("value.age" "pub.age" "priv.age" "secret-metadata.json")
for file in "${FILES[@]}"; do
if [ -f "$SECRET_DIR/$file" ]; then
print_success "Required file exists: $file"
else
print_error "Required file missing: $file"
fi
done
else
print_error "Secret directory not found"
fi
else
print_error "Default vault structure not found"
fi
# Check work vault structure
if [ -d "$TEMP_DIR/vaults.d/work" ]; then
print_success "Work vault structure exists"
else
print_error "Work vault structure not found"
fi
# Check configuration files
if [ -f "$TEMP_DIR/configuration.json" ]; then
print_success "Global configuration file exists"
else
print_warning "Global configuration file not found (may not be implemented yet)"
fi
# Check current vault symlink
if [ -L "$TEMP_DIR/currentvault" ] || [ -f "$TEMP_DIR/currentvault" ]; then
print_success "Current vault link exists"
else
print_error "Current vault link not found"
fi
# Test 13: Environment variable error handling
print_step "13" "Testing environment variable error handling"
# Test with non-existent state directory
export SB_SECRET_STATE_DIR="$TEMP_DIR/nonexistent/directory"
echo "Running: $SECRET_BINARY get \"database/password\""
if $SECRET_BINARY get "database/password"; then
print_error "Should have failed with non-existent state directory"
else
print_success "Correctly failed with non-existent state directory"
fi
# Test init with non-existent directory (should work)
echo "Running: $SECRET_BINARY init (with SB_UNLOCK_PASSPHRASE set)"
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
if $SECRET_BINARY init; then
print_success "Init works with non-existent state directory"
else
print_error "Init should work with non-existent state directory"
fi
unset SB_UNLOCK_PASSPHRASE
# Reset to working directory
export SB_SECRET_STATE_DIR="$TEMP_DIR"
# Test 14: Mixed approach compatibility
print_step "14" "Testing mixed approach compatibility"
# Verify mnemonic can access traditional secrets
RETRIEVED_MIXED=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
if [ "$RETRIEVED_MIXED" = "traditional-secret-value" ]; then
print_success "Mnemonic can access traditional secrets"
else
print_error "Mnemonic cannot access traditional secrets"
fi
# Test without mnemonic but with unlocker
echo "Testing mnemonic-created vault access..."
echo "Testing traditional unlocker access to mnemonic-created secrets..."
echo "Running: $SECRET_BINARY get test/seed (with mnemonic set)"
if RETRIEVED=$($SECRET_BINARY get test/seed 2>&1); then
print_success "Traditional unlocker can access mnemonic-created secrets"
else
print_warning "Traditional unlocker cannot access mnemonic-created secrets (may need implementation)"
fi
# Re-enable mnemonic for final tests
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
# Final summary
echo -e "\n${GREEN}=== Test Summary ===${NC}"
echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${NC}"
echo -e "${GREEN}✓ Secret manager initialization${NC}"
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
echo -e "${GREEN}✓ Import functionality with environment variable combinations${NC}"
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
echo -e "${GREEN}✓ Unlocker management (passphrase, PGP, SEP)${NC}"
echo -e "${GREEN}✓ Secret generation and storage${NC}"
echo -e "${GREEN}✓ Traditional unlocker operations${NC}"
echo -e "${GREEN}✓ Secret name validation${NC}"
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
echo -e "${GREEN}✓ Cross-vault operations${NC}"
echo -e "${GREEN}✓ Per-secret key file structure${NC}"
echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
echo -e "${GREEN}✓ Error handling${NC}"
echo -e "\n${GREEN}🎉 Comprehensive test completed with environment variable automation!${NC}"
# Show usage examples for all implemented functionality
echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"
echo -e "${YELLOW}# Environment setup:${NC}"
echo "export SB_SECRET_STATE_DIR=\"/path/to/your/secrets\""
echo "export SB_SECRET_MNEMONIC=\"your twelve word mnemonic phrase here\""
echo ""
echo -e "${YELLOW}# Initialization:${NC}"
echo "secret init"
echo ""
echo -e "${YELLOW}# Vault management:${NC}"
echo "secret vault list"
echo "secret vault create work"
echo "secret vault select work"
echo ""
echo -e "${YELLOW}# Import mnemonic (automated with environment variables):${NC}"
echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
echo "secret vault import work"
echo ""
echo -e "${YELLOW}# Unlocker management:${NC}"
echo "$SECRET_BINARY unlockers add <type> # Add unlocker (passphrase, pgp, keychain)"
echo "$SECRET_BINARY unlockers add passphrase"
echo "$SECRET_BINARY unlockers add pgp <gpg-key-id>"
echo "$SECRET_BINARY unlockers add keychain # macOS only"
echo "$SECRET_BINARY unlockers list # List all unlockers"
echo "$SECRET_BINARY unlocker select <unlocker-id> # Select current unlocker"
echo "$SECRET_BINARY unlockers rm <unlocker-id> # Remove unlocker"
echo ""
echo -e "${YELLOW}# Secret management:${NC}"
echo "echo \"my-secret\" | secret add \"app/password\""
echo "echo \"my-secret\" | secret add \"app/password\" --force"
echo "secret get \"app/password\""
echo "secret list"
echo ""
echo -e "${YELLOW}# Cross-vault operations:${NC}"
echo "secret vault select work"
echo "echo \"work-secret\" | secret add \"work/database\""
echo "secret vault select default"