add secret versioning support

This commit is contained in:
2025-06-08 22:07:19 -07:00
parent f59ee4d2d6
commit fbda2d91af
16 changed files with 2451 additions and 1608 deletions
+49 -162
View File
@@ -1,7 +1,6 @@
package secret
import (
"encoding/json"
"fmt"
"log/slog"
"os"
@@ -62,9 +61,10 @@ func NewSecret(vault VaultInterface, name string) *Secret {
}
}
// Save saves a secret value to the vault
// Save is deprecated - use vault.AddSecret directly which creates versions
// Kept for backward compatibility
func (s *Secret) Save(value []byte, force bool) error {
DebugWith("Saving secret",
DebugWith("Saving secret (deprecated method)",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
slog.Int("value_length", len(value)),
@@ -81,7 +81,7 @@ func (s *Secret) Save(value []byte, force bool) error {
return nil
}
// GetValue retrieves and decrypts the secret value using the provided unlocker
// GetValue retrieves and decrypts the current version's value using the provided unlocker
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
DebugWith("Getting secret value",
slog.String("secret_name", s.Name),
@@ -99,7 +99,17 @@ func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
return nil, fmt.Errorf("secret %s not found", s.Name)
}
Debug("Secret exists, proceeding with decryption", "secret_name", s.Name)
Debug("Secret exists, getting current version", "secret_name", s.Name)
// Get current version
currentVersion, err := GetCurrentVersion(s.vault.GetFilesystem(), s.Directory)
if err != nil {
Debug("Failed to get current version", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to get current version: %w", err)
}
// Create version object
version := NewSecretVersion(s.vault, s.Name, currentVersion)
// Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption
if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
@@ -114,8 +124,8 @@ func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
Debug("Successfully derived long-term key from mnemonic", "secret_name", s.Name)
// Use the long-term key to decrypt the secret using per-secret architecture
return s.decryptWithLongTermKey(ltIdentity)
// Use the long-term key to decrypt the version
return version.GetValue(ltIdentity)
}
Debug("Using unlocker for vault access", "secret_name", s.Name)
@@ -170,165 +180,33 @@ func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
slog.String("public_key", ltIdentity.Recipient().String()),
)
// Use the long-term key to decrypt the secret using per-secret architecture
return s.decryptWithLongTermKey(ltIdentity)
// Use the long-term key to decrypt the version
return version.GetValue(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.GetName()),
)
// 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.GetFilesystem(), 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 private key using long-term key", "secret_name", s.Name)
secretPrivKeyData, err := DecryptWithIdentity(encryptedSecretPrivKey, ltIdentity)
if err != nil {
Debug("Failed to decrypt secret private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt secret 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.GetFilesystem(), 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 value using secret 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
// LoadMetadata is deprecated - metadata is now per-version and encrypted
func (s *Secret) LoadMetadata() error {
DebugWith("Loading secret metadata",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
)
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
Debug("LoadMetadata called but is deprecated in versioned model", "secret_name", s.Name)
// For backward compatibility, we'll populate with basic info
now := time.Now()
s.Metadata = SecretMetadata{
Name: s.Name,
CreatedAt: now,
UpdatedAt: now,
}
// 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.GetFilesystem(), 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
// GetMetadata returns the secret metadata (deprecated)
func (s *Secret) GetMetadata() SecretMetadata {
Debug("Returning secret metadata", "secret_name", s.Name)
Debug("GetMetadata called but is deprecated in versioned model", "secret_name", s.Name)
return s.Metadata
}
// GetEncryptedData reads and returns the encrypted secret data
// GetEncryptedData is deprecated - data is now stored in versions
func (s *Secret) GetEncryptedData() ([]byte, error) {
DebugWith("Getting encrypted secret data",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
)
secretPath := filepath.Join(s.Directory, "value.age")
Debug("Reading encrypted secret file", "secret_path", secretPath)
encryptedData, err := afero.ReadFile(s.vault.GetFilesystem(), 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
Debug("GetEncryptedData called but is deprecated in versioned model", "secret_name", s.Name)
return nil, fmt.Errorf("GetEncryptedData is deprecated - use version-specific methods")
}
// Exists checks if the secret exists on disk
@@ -338,22 +216,31 @@ func (s *Secret) Exists() (bool, error) {
slog.String("vault_name", s.vault.GetName()),
)
secretPath := filepath.Join(s.Directory, "value.age")
Debug("Checking secret file existence", "secret_path", secretPath)
exists, err := afero.Exists(s.vault.GetFilesystem(), secretPath)
// Check if the secret directory exists and has a current symlink
exists, err := afero.DirExists(s.vault.GetFilesystem(), s.Directory)
if err != nil {
Debug("Failed to check secret file existence", "error", err, "secret_path", secretPath)
Debug("Failed to check secret directory existence", "error", err, "secret_dir", s.Directory)
return false, err
}
if !exists {
Debug("Secret directory does not exist", "secret_dir", s.Directory)
return false, nil
}
// Check if current symlink exists
_, err = GetCurrentVersion(s.vault.GetFilesystem(), s.Directory)
if err != nil {
Debug("No current version found", "error", err, "secret_name", s.Name)
return false, nil
}
DebugWith("Secret existence check result",
slog.String("secret_name", s.Name),
slog.Bool("exists", exists),
slog.Bool("exists", true),
)
return exists, nil
return true, nil
}
// GetCurrentVault gets the current vault from the file system