refactor: Implement proper separation between unlock keys and secret decryption - Remove DecryptSecret methods from all unlock key implementations - Secrets now handle their own decryption via Secret.GetValue(unlockKey) - Unlock keys are only responsible for vault access (getting long-term key) - Add decryptWithLongTermKey helper for per-secret key architecture - Fix vault import to work in non-interactive mode without unlock keys - Maintain clean architecture: unlock keys → vault access → secret decryption - All tests passing with new architecture

This commit is contained in:
2025-05-29 10:06:30 -07:00
parent 4b59d6fb82
commit 345709a306
6 changed files with 146 additions and 363 deletions
+116 -38
View File
@@ -9,6 +9,7 @@ import (
"strings"
"time"
"filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/spf13/afero"
)
@@ -92,7 +93,7 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
// 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)
Debug("Using mnemonic from environment for direct long-term key derivation", "secret_name", s.Name)
// Use mnemonic directly to derive long-term key
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
@@ -103,62 +104,139 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
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
// Use the long-term key to decrypt the secret using per-secret architecture
return s.decryptWithLongTermKey(ltIdentity)
}
Debug("Using unlock key for secret decryption", "secret_name", s.Name)
Debug("Using unlock key for vault access", "secret_name", s.Name)
// Use the provided unlock key to decrypt the secret
// Use the provided unlock key to get the vault's long-term private key
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",
DebugWith("Getting vault's long-term key using 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)
// Step 1: Use the unlock key to get the vault's long-term private key
unlockIdentity, err := unlockKey.GetIdentity()
if err != nil {
Debug("Unlock key failed to decrypt secret", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
return nil, err
Debug("Failed to get unlock key identity", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
}
DebugWith("Successfully decrypted secret via unlock key",
// Read the encrypted long-term private key from the unlock key directory
encryptedLtPrivKeyPath := filepath.Join(unlockKey.GetDirectory(), "longterm.age")
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
encryptedLtPrivKey, err := afero.ReadFile(s.vault.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)
}
// Decrypt the vault's long-term private key using the unlock key
Debug("Decrypting vault's long-term private key with unlock key", "secret_name", s.Name)
ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
if err != nil {
Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
// Parse the long-term private key
Debug("Parsing long-term private key", "secret_name", s.Name)
ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
if err != nil {
Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
}
DebugWith("Successfully obtained vault's long-term key",
slog.String("secret_name", s.Name),
slog.String("unlock_key_type", unlockKey.GetType()),
slog.Int("decrypted_length", len(decryptedData)),
slog.String("public_key", ltIdentity.Recipient().String()),
)
return decryptedData, nil
// Use the long-term key to decrypt the secret using per-secret architecture
return s.decryptWithLongTermKey(ltIdentity)
}
// decryptWithLongTermKey decrypts the secret using the vault's long-term private key
// This implements the per-secret key architecture: longterm -> secret private key -> secret value
func (s *Secret) decryptWithLongTermKey(ltIdentity *age.X25519Identity) ([]byte, error) {
DebugWith("Decrypting secret with long-term key using per-secret architecture",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
)
// Step 1: Read the secret's encrypted private key from priv.age
encryptedSecretPrivKeyPath := filepath.Join(s.Directory, "priv.age")
Debug("Reading encrypted secret private key", "path", encryptedSecretPrivKeyPath)
encryptedSecretPrivKey, err := afero.ReadFile(s.vault.fs, encryptedSecretPrivKeyPath)
if err != nil {
Debug("Failed to read encrypted secret private key", "error", err, "path", encryptedSecretPrivKeyPath)
return nil, fmt.Errorf("failed to read encrypted secret private key: %w", err)
}
DebugWith("Read encrypted secret private key",
slog.String("secret_name", s.Name),
slog.Int("encrypted_length", len(encryptedSecretPrivKey)),
)
// Step 2: Decrypt the secret's private key using the vault's long-term private key
Debug("Decrypting secret's private key with vault's long-term key", "secret_name", s.Name)
secretPrivKeyData, err := decryptWithIdentity(encryptedSecretPrivKey, ltIdentity)
if err != nil {
Debug("Failed to decrypt secret's private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt secret's private key: %w", err)
}
// Parse the secret's private key
Debug("Parsing secret's private key", "secret_name", s.Name)
secretIdentity, err := age.ParseX25519Identity(string(secretPrivKeyData))
if err != nil {
Debug("Failed to parse secret's private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to parse secret's private key: %w", err)
}
DebugWith("Successfully decrypted and parsed secret's identity",
slog.String("secret_name", s.Name),
slog.String("secret_public_key", secretIdentity.Recipient().String()),
)
// Step 3: Read the secret's encrypted value from value.age
encryptedValuePath := filepath.Join(s.Directory, "value.age")
Debug("Reading encrypted secret value", "path", encryptedValuePath)
encryptedValue, err := afero.ReadFile(s.vault.fs, encryptedValuePath)
if err != nil {
Debug("Failed to read encrypted secret value", "error", err, "path", encryptedValuePath)
return nil, fmt.Errorf("failed to read encrypted secret value: %w", err)
}
DebugWith("Read encrypted secret value",
slog.String("secret_name", s.Name),
slog.Int("encrypted_length", len(encryptedValue)),
)
// Step 4: Decrypt the secret's value using the secret's private key
Debug("Decrypting secret value with secret's private key", "secret_name", s.Name)
decryptedValue, err := decryptWithIdentity(encryptedValue, secretIdentity)
if err != nil {
Debug("Failed to decrypt secret value", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt secret value: %w", err)
}
DebugWith("Successfully decrypted secret value using per-secret key architecture",
slog.String("secret_name", s.Name),
slog.Int("decrypted_length", len(decryptedValue)),
)
return decryptedValue, nil
}
// LoadMetadata loads the secret metadata from disk