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:
+116
-38
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user