add secret versioning support
This commit is contained in:
+49
-162
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user