restored from backups

This commit is contained in:
Jeffrey Paul 2025-05-29 08:30:16 -07:00
parent 8c08c2e748
commit 3d90388b5b
8 changed files with 708 additions and 212 deletions

View File

@ -1,5 +1,7 @@
package main
import "git.eeqj.de/sneak/secret/internal/secret"
func main() {
CLIEntry()
secret.CLIEntry()
}

133
internal/secret/debug.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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))

View File

@ -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

269
internal/secret/secret.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}