passes tests now!
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
currentUnlockerContent := readFile(t, currentUnlockerFile)
|
||||
// The file likely contains the unlocker ID
|
||||
assert.Contains(t, string(currentUnlockerContent), "passphrase", "current unlocker should be passphrase type")
|
||||
// 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)
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
|
||||
// VaultMetadata contains information about a vault
|
||||
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
|
||||
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 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -247,9 +251,10 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
||||
|
||||
// Save vault metadata
|
||||
metadata := &VaultMetadata{
|
||||
CreatedAt: time.Now(),
|
||||
DerivationIndex: derivationIndex,
|
||||
PublicKeyHash: publicKeyHash,
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,9 @@ func TestVaultMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
metadata1 := &VaultMetadata{
|
||||
DerivationIndex: 0,
|
||||
PublicKeyHash: pubKeyHash0,
|
||||
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)
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user