diff --git a/cmd/secret/main.go b/cmd/secret/main.go index 615029c..ff6f145 100644 --- a/cmd/secret/main.go +++ b/cmd/secret/main.go @@ -1,5 +1,7 @@ package main +import "git.eeqj.de/sneak/secret/internal/secret" + func main() { - CLIEntry() + secret.CLIEntry() } diff --git a/internal/secret/debug.go b/internal/secret/debug.go new file mode 100644 index 0000000..2935022 --- /dev/null +++ b/internal/secret/debug.go @@ -0,0 +1,133 @@ +package secret + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "strings" + "syscall" + + "golang.org/x/term" +) + +var ( + debugEnabled bool + debugLogger *slog.Logger +) + +func init() { + initDebugLogging() +} + +// initDebugLogging initializes the debug logging system based on GODEBUG environment variable +func initDebugLogging() { + godebug := os.Getenv("GODEBUG") + debugEnabled = strings.Contains(godebug, "berlin.sneak.pkg.secret") + + if !debugEnabled { + // Create a no-op logger that discards all output + debugLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) + return + } + + // Check if STDERR is a TTY + isTTY := term.IsTerminal(int(syscall.Stderr)) + + var handler slog.Handler + if isTTY { + // TTY output: colorized structured format + handler = newColorizedHandler(os.Stderr) + } else { + // Non-TTY output: JSON Lines format + handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + } + + debugLogger = slog.New(handler) +} + +// IsDebugEnabled returns true if debug logging is enabled +func IsDebugEnabled() bool { + return debugEnabled +} + +// Debug logs a debug message with optional attributes +func Debug(msg string, args ...any) { + if !debugEnabled { + return + } + debugLogger.Debug(msg, args...) +} + +// DebugF logs a formatted debug message with optional attributes +func DebugF(format string, args ...any) { + if !debugEnabled { + return + } + debugLogger.Debug(fmt.Sprintf(format, args...)) +} + +// DebugWith logs a debug message with structured attributes +func DebugWith(msg string, attrs ...slog.Attr) { + if !debugEnabled { + return + } + debugLogger.LogAttrs(context.Background(), slog.LevelDebug, msg, attrs...) +} + +// colorizedHandler implements a TTY-friendly structured log handler +type colorizedHandler struct { + output io.Writer +} + +func newColorizedHandler(output io.Writer) slog.Handler { + return &colorizedHandler{output: output} +} + +func (h *colorizedHandler) Enabled(_ context.Context, level slog.Level) bool { + // Explicitly check that debug is enabled AND the level is DEBUG or higher + // This ensures we don't default to INFO level when debug is enabled + return debugEnabled && level >= slog.LevelDebug +} + +func (h *colorizedHandler) Handle(_ context.Context, record slog.Record) error { + if !debugEnabled { + return nil + } + + // Format: [DEBUG] message {key=value, key2=value2} + output := fmt.Sprintf("\033[36m[DEBUG]\033[0m \033[1m%s\033[0m", record.Message) + + if record.NumAttrs() > 0 { + output += " \033[33m{" + first := true + record.Attrs(func(attr slog.Attr) bool { + if !first { + output += ", " + } + first = false + output += fmt.Sprintf("%s=%#v", attr.Key, attr.Value.Any()) + return true + }) + output += "}\033[0m" + } + + output += "\n" + _, err := h.output.Write([]byte(output)) + return err +} + +func (h *colorizedHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // For simplicity, return the same handler + // In a more complex implementation, we'd create a new handler with the attrs + return h +} + +func (h *colorizedHandler) WithGroup(name string) slog.Handler { + // For simplicity, return the same handler + // In a more complex implementation, we'd create a new handler with the group + return h +} \ No newline at end of file diff --git a/internal/secret/keychainunlock.go b/internal/secret/keychainunlock.go index 38027b8..69b1a80 100644 --- a/internal/secret/keychainunlock.go +++ b/internal/secret/keychainunlock.go @@ -11,6 +11,7 @@ import ( "time" "filippo.io/age" + "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" ) @@ -175,61 +176,6 @@ func (k *KeychainUnlockKey) Remove() error { return nil } -// DecryptLongTermKey decrypts and returns the long-term private key for this vault -func (k *KeychainUnlockKey) DecryptLongTermKey() ([]byte, error) { - DebugWith("Decrypting long-term key with keychain unlock key", - slog.String("key_id", k.GetID()), - slog.String("key_type", k.GetType()), - ) - - // Get keychain item name and retrieve data - keychainItemName, err := k.GetKeychainItemName() - if err != nil { - Debug("Failed to get keychain item name for long-term decryption", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to get keychain item name: %w", err) - } - - keychainDataBytes, err := retrieveFromKeychain(keychainItemName) - if err != nil { - Debug("Failed to retrieve data from keychain for long-term decryption", "error", err, "keychain_item", keychainItemName) - return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err) - } - - var keychainData KeychainData - if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil { - Debug("Failed to parse keychain data for long-term decryption", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to parse keychain data: %w", err) - } - - // Decrypt the long-term private key using the encrypted data from keychain - encryptedLtPrivKey, err := hex.DecodeString(keychainData.EncryptedLongtermKey) - if err != nil { - Debug("Failed to decode encrypted long-term key from keychain", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to decode encrypted long-term key: %w", err) - } - - // Get our unlock key identity to decrypt the long-term key - unlockIdentity, err := k.GetIdentity() - if err != nil { - Debug("Failed to get keychain unlock identity for long-term decryption", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to get unlock identity: %w", err) - } - - // Decrypt long-term private key using our unlock key - ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity) - if err != nil { - Debug("Failed to decrypt long-term private key with keychain unlock key", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) - } - - DebugWith("Successfully decrypted long-term private key with keychain unlock key", - slog.String("key_id", k.GetID()), - slog.Int("decrypted_length", len(ltPrivKeyData)), - ) - - return ltPrivKeyData, nil -} - // DecryptSecret decrypts a secret using this keychain unlock key's long-term key management func (k *KeychainUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { DebugWith("Decrypting secret with keychain unlock key", @@ -251,11 +197,51 @@ func (k *KeychainUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { slog.Int("encrypted_length", len(encryptedData)), ) - // Decrypt long-term private key using our unlock key - ltPrivKeyData, err := k.DecryptLongTermKey() - if err != nil { - Debug("Failed to decrypt long-term private key for secret decryption", "error", err, "key_id", k.GetID()) - return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + // Get or derive the long-term private key + var ltPrivKeyData []byte + + // Check if mnemonic is available in environment variable + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + ltPrivKeyData = []byte(ltIdentity.String()) + } else { + // Get keychain item name and retrieve data + keychainItemName, err := k.GetKeychainItemName() + if err != nil { + return nil, fmt.Errorf("failed to get keychain item name: %w", err) + } + + keychainDataBytes, err := retrieveFromKeychain(keychainItemName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err) + } + + var keychainData KeychainData + if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil { + return nil, fmt.Errorf("failed to parse keychain data: %w", err) + } + + // Decrypt the long-term private key using the encrypted data from keychain + encryptedLtPrivKey, err := hex.DecodeString(keychainData.EncryptedLongtermKey) + if err != nil { + return nil, fmt.Errorf("failed to decode encrypted long-term key: %w", err) + } + + // Get our unlock key identity to decrypt the long-term key + unlockIdentity, err := k.GetIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get unlock identity: %w", err) + } + + // Decrypt long-term private key using our unlock key + ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, unlockIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } } // Parse long-term private key @@ -388,9 +374,62 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, } // Step 5: Get or derive the long-term private key - ltPrivKeyData, err := vault.GetLongTermKey() - if err != nil { - return nil, fmt.Errorf("failed to get long-term private key: %w", err) + var ltPrivKeyData []byte + + // Check if mnemonic is available in environment variable + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + ltPrivKeyData = []byte(ltIdentity.String()) + } else { + // Get the vault to access current unlock key + currentUnlockKey, err := vault.GetCurrentUnlockKey() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key: %w", err) + } + + // Get the current unlock key identity + currentUnlockIdentity, err := currentUnlockKey.GetIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key identity: %w", err) + } + + // Get encrypted long-term key from current unlock key, handling different types + var encryptedLtPrivKey []byte + switch currentUnlockKey := currentUnlockKey.(type) { + case *PassphraseUnlockKey: + // Read the encrypted long-term private key from passphrase unlock key + encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err) + } + + case *PGPUnlockKey: + // Read the encrypted long-term private key from PGP unlock key + encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err) + } + + case *KeychainUnlockKey: + // Read the encrypted long-term private key from another keychain unlock key + encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlock key: %w", err) + } + + default: + return nil, fmt.Errorf("unsupported current unlock key type for keychain unlock key creation") + } + + // Decrypt long-term private key using current unlock key + ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } } // Step 6: Encrypt long-term private key to the new age unlock key diff --git a/internal/secret/passphraseunlock.go b/internal/secret/passphraseunlock.go index b2a4a8a..5792c79 100644 --- a/internal/secret/passphraseunlock.go +++ b/internal/secret/passphraseunlock.go @@ -141,17 +141,32 @@ func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*Pass return currentVault.CreatePassphraseKey(passphrase) } -// DecryptLongTermKey decrypts and returns the long-term private key for this vault -func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) { - DebugWith("Decrypting long-term key with passphrase unlock key", +// DecryptSecret decrypts a secret using this passphrase unlock key's long-term key management +func (p *PassphraseUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { + DebugWith("Decrypting secret with passphrase unlock key", + slog.String("secret_name", secret.Name), slog.String("key_id", p.GetID()), slog.String("key_type", p.GetType()), ) - // Get our unlock key identity + // Get our unlock key encrypted data + encryptedData, err := secret.GetEncryptedData() + if err != nil { + Debug("Failed to get encrypted secret data for passphrase decryption", "error", err, "secret_name", secret.Name) + return nil, fmt.Errorf("failed to get encrypted secret data: %w", err) + } + + DebugWith("Retrieved encrypted secret data for passphrase decryption", + slog.String("secret_name", secret.Name), + slog.String("key_id", p.GetID()), + slog.Int("encrypted_length", len(encryptedData)), + ) + + // Get our age identity + Debug("Getting passphrase unlock key identity for secret decryption", "key_id", p.GetID()) unlockIdentity, err := p.GetIdentity() if err != nil { - Debug("Failed to get passphrase unlock identity for long-term decryption", "error", err, "key_id", p.GetID()) + Debug("Failed to get passphrase unlock identity", "error", err, "key_id", p.GetID()) return nil, fmt.Errorf("failed to get unlock identity: %w", err) } @@ -183,37 +198,6 @@ func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) { slog.Int("decrypted_length", len(ltPrivKeyData)), ) - return ltPrivKeyData, nil -} - -// DecryptSecret decrypts a secret using this passphrase unlock key's long-term key management -func (p *PassphraseUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { - DebugWith("Decrypting secret with passphrase unlock key", - slog.String("secret_name", secret.Name), - slog.String("key_id", p.GetID()), - slog.String("key_type", p.GetType()), - ) - - // Get our unlock key encrypted data - encryptedData, err := secret.GetEncryptedData() - if err != nil { - Debug("Failed to get encrypted secret data for passphrase decryption", "error", err, "secret_name", secret.Name) - return nil, fmt.Errorf("failed to get encrypted secret data: %w", err) - } - - DebugWith("Retrieved encrypted secret data for passphrase decryption", - slog.String("secret_name", secret.Name), - slog.String("key_id", p.GetID()), - slog.Int("encrypted_length", len(encryptedData)), - ) - - // Decrypt long-term private key using our unlock key - ltPrivKeyData, err := p.DecryptLongTermKey() - if err != nil { - Debug("Failed to decrypt long-term private key for secret decryption", "error", err, "key_id", p.GetID()) - return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) - } - // Parse long-term private key Debug("Parsing long-term private key", "key_id", p.GetID()) ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData)) diff --git a/internal/secret/pgpunlock.go b/internal/secret/pgpunlock.go index b116d01..6a5cbe9 100644 --- a/internal/secret/pgpunlock.go +++ b/internal/secret/pgpunlock.go @@ -11,6 +11,7 @@ import ( "time" "filippo.io/age" + "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" ) @@ -123,51 +124,6 @@ func (p *PGPUnlockKey) Remove() error { return nil } -// DecryptLongTermKey decrypts and returns the long-term private key for this vault -func (p *PGPUnlockKey) DecryptLongTermKey() ([]byte, error) { - DebugWith("Decrypting long-term key with PGP unlock key", - slog.String("key_id", p.GetID()), - slog.String("key_type", p.GetType()), - ) - - // Get our age identity - unlockIdentity, err := p.GetIdentity() - if err != nil { - Debug("Failed to get PGP unlock identity for long-term decryption", "error", err, "key_id", p.GetID()) - return nil, fmt.Errorf("failed to get unlock identity: %w", err) - } - - // Read encrypted long-term private key - encryptedLtPrivKeyPath := filepath.Join(p.Directory, "longterm.age") - Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath) - - encryptedLtPrivKey, err := afero.ReadFile(p.fs, encryptedLtPrivKeyPath) - if err != nil { - Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath) - return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err) - } - - DebugWith("Read encrypted long-term private key", - slog.String("key_id", p.GetID()), - slog.Int("encrypted_length", len(encryptedLtPrivKey)), - ) - - // Decrypt long-term private key using our unlock key - Debug("Decrypting long-term private key with PGP unlock key", "key_id", p.GetID()) - ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity) - if err != nil { - Debug("Failed to decrypt long-term private key", "error", err, "key_id", p.GetID()) - return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) - } - - DebugWith("Successfully decrypted long-term private key", - slog.String("key_id", p.GetID()), - slog.Int("decrypted_length", len(ltPrivKeyData)), - ) - - return ltPrivKeyData, nil -} - // DecryptSecret decrypts a secret using this PGP unlock key's long-term key management func (p *PGPUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { DebugWith("Decrypting secret with PGP unlock key", @@ -189,11 +145,71 @@ func (p *PGPUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { slog.Int("encrypted_length", len(encryptedData)), ) - // Decrypt long-term private key using our unlock key - ltPrivKeyData, err := p.DecryptLongTermKey() + // Get our age identity + Debug("Getting PGP unlock key identity for secret decryption", "key_id", p.GetID()) + _, err = p.GetIdentity() if err != nil { - Debug("Failed to decrypt long-term private key for secret decryption", "error", err, "key_id", p.GetID()) - return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + Debug("Failed to get PGP unlock identity", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to get unlock identity: %w", err) + } + + // Get or derive the long-term private key + var ltPrivKeyData []byte + + // Check if mnemonic is available in environment variable + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + ltPrivKeyData = []byte(ltIdentity.String()) + } else { + // Get the vault to access current unlock key + stateDir := filepath.Dir(filepath.Dir(filepath.Dir(p.Directory))) + vault, err := GetCurrentVault(p.fs, stateDir) + if err != nil { + return nil, fmt.Errorf("failed to get vault: %w", err) + } + + // Get current unlock key + currentUnlockKey, err := vault.GetCurrentUnlockKey() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key: %w", err) + } + + // Get the current unlock key identity + currentUnlockIdentity, err := currentUnlockKey.GetIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key identity: %w", err) + } + + // Get encrypted long-term key from current unlock key, handling different types + var encryptedLtPrivKey []byte + switch currentUnlockKey := currentUnlockKey.(type) { + case *PassphraseUnlockKey: + // Read the encrypted long-term private key from passphrase unlock key + encryptedLtPrivKey, err = afero.ReadFile(p.fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err) + } + + case *PGPUnlockKey: + // Read the encrypted long-term private key from PGP unlock key + encryptedLtPrivKey, err = afero.ReadFile(p.fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err) + } + + default: + return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation") + } + + // Decrypt long-term private key using current unlock key + ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } } // Parse long-term private key @@ -308,9 +324,55 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo } // Step 3: Get or derive the long-term private key - ltPrivKeyData, err := vault.GetLongTermKey() - if err != nil { - return nil, fmt.Errorf("failed to get long-term private key: %w", err) + var ltPrivKeyData []byte + + // Check if mnemonic is available in environment variable + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + ltPrivKeyData = []byte(ltIdentity.String()) + } else { + // Get the vault to access current unlock key + currentUnlockKey, err := vault.GetCurrentUnlockKey() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key: %w", err) + } + + // Get the current unlock key identity + currentUnlockIdentity, err := currentUnlockKey.GetIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get current unlock key identity: %w", err) + } + + // Get encrypted long-term key from current unlock key, handling different types + var encryptedLtPrivKey []byte + switch currentUnlockKey := currentUnlockKey.(type) { + case *PassphraseUnlockKey: + // Read the encrypted long-term private key from passphrase unlock key + encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err) + } + + case *PGPUnlockKey: + // Read the encrypted long-term private key from PGP unlock key + encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err) + } + + default: + return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation") + } + + // Decrypt long-term private key using current unlock key + ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } } // Step 4: Encrypt long-term private key to the new age unlock key diff --git a/internal/secret/secret.go b/internal/secret/secret.go new file mode 100644 index 0000000..409a3b6 --- /dev/null +++ b/internal/secret/secret.go @@ -0,0 +1,269 @@ +package secret + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + "git.eeqj.de/sneak/secret/pkg/agehd" + "github.com/spf13/afero" +) + +// Secret represents a secret in a vault +type Secret struct { + Name string + Directory string + Metadata SecretMetadata + vault *Vault +} + +// NewSecret creates a new Secret instance +func NewSecret(vault *Vault, name string) *Secret { + DebugWith("Creating new secret instance", + slog.String("secret_name", name), + slog.String("vault_name", vault.Name), + ) + + // Convert slashes to percent signs for storage directory name + storageName := strings.ReplaceAll(name, "/", "%") + vaultDir, _ := vault.GetDirectory() + secretDir := filepath.Join(vaultDir, "secrets.d", storageName) + + DebugWith("Secret storage details", + slog.String("secret_name", name), + slog.String("storage_name", storageName), + slog.String("secret_dir", secretDir), + ) + + return &Secret{ + Name: name, + Directory: secretDir, + vault: vault, + Metadata: SecretMetadata{ + Name: name, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } +} + +// Save saves a secret value to the vault +func (s *Secret) Save(value []byte, force bool) error { + DebugWith("Saving secret", + slog.String("secret_name", s.Name), + slog.String("vault_name", s.vault.Name), + slog.Int("value_length", len(value)), + slog.Bool("force", force), + ) + + err := s.vault.AddSecret(s.Name, value, force) + if err != nil { + Debug("Failed to save secret", "error", err, "secret_name", s.Name) + return err + } + + Debug("Successfully saved secret", "secret_name", s.Name) + return nil +} + +// GetValue retrieves and decrypts the secret value using the provided unlock key +func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) { + DebugWith("Getting secret value", + slog.String("secret_name", s.Name), + slog.String("vault_name", s.vault.Name), + ) + + // Check if secret exists + exists, err := s.Exists() + if err != nil { + Debug("Failed to check if secret exists during GetValue", "error", err, "secret_name", s.Name) + return nil, fmt.Errorf("failed to check if secret exists: %w", err) + } + if !exists { + Debug("Secret not found during GetValue", "secret_name", s.Name, "vault_name", s.vault.Name) + return nil, fmt.Errorf("secret %s not found", s.Name) + } + + Debug("Secret exists, proceeding with decryption", "secret_name", s.Name) + + // Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + Debug("Using mnemonic from environment for secret decryption", "secret_name", s.Name) + + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + Debug("Failed to derive long-term key from mnemonic for secret", "error", err, "secret_name", s.Name) + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + + Debug("Successfully derived long-term key from mnemonic", "secret_name", s.Name) + + // Read our own encrypted data + encryptedData, err := s.GetEncryptedData() + if err != nil { + Debug("Failed to get encrypted data for mnemonic decryption", "error", err, "secret_name", s.Name) + return nil, err + } + + DebugWith("Retrieved encrypted data for mnemonic decryption", + slog.String("secret_name", s.Name), + slog.Int("encrypted_length", len(encryptedData)), + ) + + // Decrypt secret data + Debug("Decrypting secret with long-term key from mnemonic", "secret_name", s.Name) + decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity) + if err != nil { + Debug("Failed to decrypt secret with mnemonic", "error", err, "secret_name", s.Name) + return nil, fmt.Errorf("failed to decrypt secret: %w", err) + } + + DebugWith("Successfully decrypted secret with mnemonic", + slog.String("secret_name", s.Name), + slog.Int("decrypted_length", len(decryptedData)), + ) + + return decryptedData, nil + } + + Debug("Using unlock key for secret decryption", "secret_name", s.Name) + + // Use the provided unlock key to decrypt the secret + if unlockKey == nil { + Debug("No unlock key provided for secret decryption", "secret_name", s.Name) + return nil, fmt.Errorf("unlock key required to decrypt secret") + } + + DebugWith("Delegating secret decryption to unlock key", + slog.String("secret_name", s.Name), + slog.String("unlock_key_type", unlockKey.GetType()), + slog.String("unlock_key_id", unlockKey.GetID()), + ) + + // Delegate decryption to the unlock key implementation + decryptedData, err := unlockKey.DecryptSecret(s) + if err != nil { + Debug("Unlock key failed to decrypt secret", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType()) + return nil, err + } + + DebugWith("Successfully decrypted secret via unlock key", + slog.String("secret_name", s.Name), + slog.String("unlock_key_type", unlockKey.GetType()), + slog.Int("decrypted_length", len(decryptedData)), + ) + + return decryptedData, nil +} + +// LoadMetadata loads the secret metadata from disk +func (s *Secret) LoadMetadata() error { + DebugWith("Loading secret metadata", + slog.String("secret_name", s.Name), + slog.String("vault_name", s.vault.Name), + ) + + vaultDir, err := s.vault.GetDirectory() + if err != nil { + Debug("Failed to get vault directory for metadata loading", "error", err, "secret_name", s.Name) + return err + } + + // Convert slashes to percent signs for storage + storageName := strings.ReplaceAll(s.Name, "/", "%") + metadataPath := filepath.Join(vaultDir, "secrets.d", storageName, "secret-metadata.json") + + DebugWith("Reading secret metadata", + slog.String("secret_name", s.Name), + slog.String("metadata_path", metadataPath), + ) + + // Read metadata file + metadataBytes, err := afero.ReadFile(s.vault.fs, metadataPath) + if err != nil { + Debug("Failed to read secret metadata file", "error", err, "metadata_path", metadataPath) + return fmt.Errorf("failed to read metadata: %w", err) + } + + DebugWith("Read secret metadata file", + slog.String("secret_name", s.Name), + slog.Int("metadata_size", len(metadataBytes)), + ) + + var metadata SecretMetadata + if err := json.Unmarshal(metadataBytes, &metadata); err != nil { + Debug("Failed to parse secret metadata JSON", "error", err, "secret_name", s.Name) + return fmt.Errorf("failed to parse metadata: %w", err) + } + + DebugWith("Parsed secret metadata", + slog.String("secret_name", metadata.Name), + slog.Time("created_at", metadata.CreatedAt), + slog.Time("updated_at", metadata.UpdatedAt), + ) + + s.Metadata = metadata + Debug("Successfully loaded secret metadata", "secret_name", s.Name) + return nil +} + +// GetMetadata returns the secret metadata +func (s *Secret) GetMetadata() SecretMetadata { + Debug("Returning secret metadata", "secret_name", s.Name) + return s.Metadata +} + +// GetEncryptedData reads and returns the encrypted secret data +func (s *Secret) GetEncryptedData() ([]byte, error) { + DebugWith("Getting encrypted secret data", + slog.String("secret_name", s.Name), + slog.String("vault_name", s.vault.Name), + ) + + secretPath := filepath.Join(s.Directory, "secret.age") + + Debug("Reading encrypted secret file", "secret_path", secretPath) + + encryptedData, err := afero.ReadFile(s.vault.fs, secretPath) + if err != nil { + Debug("Failed to read encrypted secret file", "error", err, "secret_path", secretPath) + return nil, fmt.Errorf("failed to read encrypted secret: %w", err) + } + + DebugWith("Successfully read encrypted secret data", + slog.String("secret_name", s.Name), + slog.Int("encrypted_length", len(encryptedData)), + ) + + return encryptedData, nil +} + +// Exists checks if the secret exists on disk +func (s *Secret) Exists() (bool, error) { + DebugWith("Checking if secret exists", + slog.String("secret_name", s.Name), + slog.String("vault_name", s.vault.Name), + ) + + secretPath := filepath.Join(s.Directory, "secret.age") + + Debug("Checking secret file existence", "secret_path", secretPath) + + exists, err := afero.Exists(s.vault.fs, secretPath) + if err != nil { + Debug("Failed to check secret file existence", "error", err, "secret_path", secretPath) + return false, err + } + + DebugWith("Secret existence check result", + slog.String("secret_name", s.Name), + slog.Bool("exists", exists), + ) + + return exists, nil +} \ No newline at end of file diff --git a/internal/secret/unlock.go b/internal/secret/unlock.go index bd12048..00ca4f6 100644 --- a/internal/secret/unlock.go +++ b/internal/secret/unlock.go @@ -14,9 +14,6 @@ type UnlockKey interface { ID() string // Generate ID from the key's public key Remove() error // Remove the unlock key and any associated resources - // DecryptLongTermKey decrypts and returns the long-term private key for this vault - DecryptLongTermKey() ([]byte, error) - // DecryptSecret decrypts a secret using this unlock key's long-term key management DecryptSecret(secret *Secret) ([]byte, error) } diff --git a/internal/secret/vault.go b/internal/secret/vault.go index c7d1716..2aa4c2e 100644 --- a/internal/secret/vault.go +++ b/internal/secret/vault.go @@ -924,9 +924,73 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, er } // Get or derive the long-term private key - ltPrivKeyData, err := v.GetLongTermKey() - if err != nil { - return nil, fmt.Errorf("failed to get long-term private key: %w", err) + var ltPrivKeyData []byte + + // Check if mnemonic is available in environment variable + if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { + // Use mnemonic directly to derive long-term key + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + if err != nil { + return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) + } + ltPrivKeyData = []byte(ltIdentity.String()) + } else { + // Try to get the long-term private key from the current unlock key + currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key") + + // Check if current unlock key exists + _, err := v.fs.Stat(currentUnlockKeyPath) + if err != nil { + return nil, fmt.Errorf("no current unlock key found and no mnemonic available in environment. Set SB_SECRET_MNEMONIC or ensure a current unlock key exists") + } + + // Resolve the current unlock key path + var currentUnlockKeyDir string + if _, ok := v.fs.(*afero.OsFs); ok { + // For real filesystems, resolve the symlink properly + currentUnlockKeyDir, err = resolveVaultSymlink(v.fs, currentUnlockKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err) + } + } else { + // Fallback for mock filesystems: read the path from file contents + currentUnlockKeyTarget, err := afero.ReadFile(v.fs, currentUnlockKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to read current unlock key: %w", err) + } + currentUnlockKeyDir = strings.TrimSpace(string(currentUnlockKeyTarget)) + } + + // Read the current unlock key's encrypted private key + currentEncPrivKeyData, err := afero.ReadFile(v.fs, filepath.Join(currentUnlockKeyDir, "priv.age")) + if err != nil { + return nil, fmt.Errorf("failed to read current unlock key private key: %w", err) + } + + // Decrypt the current unlock key private key with the same passphrase + // (assuming the user wants to use the same passphrase for the new key) + currentPrivKeyData, err := decryptWithPassphrase(currentEncPrivKeyData, passphrase) + if err != nil { + return nil, fmt.Errorf("failed to decrypt current unlock key private key: %w", err) + } + + // Parse the current unlock key + currentIdentity, err := age.ParseX25519Identity(string(currentPrivKeyData)) + if err != nil { + return nil, fmt.Errorf("failed to parse current unlock key: %w", err) + } + + // Read the encrypted long-term private key + encryptedLtPrivKey, err := afero.ReadFile(v.fs, filepath.Join(currentUnlockKeyDir, "longterm.age")) + if err != nil { + return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err) + } + + // Decrypt the long-term private key using the current unlock key + ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } } // Encrypt the long-term private key to the new unlock key @@ -985,57 +1049,3 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, er fs: v.fs, }, nil } - -// GetLongTermKey returns the long-term private key for this vault -func (v *Vault) GetLongTermKey() ([]byte, error) { - DebugWith("Getting long-term key for vault", slog.String("vault_name", v.Name)) - - // Check if mnemonic is available in environment variable for direct derivation - if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" { - Debug("Using mnemonic from environment to derive long-term key", "vault_name", v.Name) - - // Use mnemonic directly to derive long-term key - ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) - if err != nil { - Debug("Failed to derive long-term key from mnemonic", "error", err, "vault_name", v.Name) - return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) - } - - ltPrivKeyData := []byte(ltIdentity.String()) - DebugWith("Successfully derived long-term key from mnemonic", - slog.String("vault_name", v.Name), - slog.Int("key_length", len(ltPrivKeyData)), - ) - return ltPrivKeyData, nil - } - - Debug("Using current unlock key to decrypt long-term key", "vault_name", v.Name) - - // Get current unlock key - currentUnlockKey, err := v.GetCurrentUnlockKey() - if err != nil { - Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name) - return nil, fmt.Errorf("failed to get current unlock key: %w", err) - } - - DebugWith("Retrieved current unlock key for long-term decryption", - slog.String("vault_name", v.Name), - slog.String("unlock_key_type", currentUnlockKey.GetType()), - slog.String("unlock_key_id", currentUnlockKey.GetID()), - ) - - // Use the unlock key's DecryptLongTermKey method - ltPrivKeyData, err := currentUnlockKey.DecryptLongTermKey() - if err != nil { - Debug("Failed to decrypt long-term key with current unlock key", "error", err, "vault_name", v.Name) - return nil, fmt.Errorf("failed to decrypt long-term key: %w", err) - } - - DebugWith("Successfully decrypted long-term key via current unlock key", - slog.String("vault_name", v.Name), - slog.String("unlock_key_type", currentUnlockKey.GetType()), - slog.Int("key_length", len(ltPrivKeyData)), - ) - - return ltPrivKeyData, nil -}