267 lines
9.0 KiB
Go
267 lines
9.0 KiB
Go
package secret
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// VaultInterface defines the interface that vault implementations must satisfy
|
|
type VaultInterface interface {
|
|
GetDirectory() (string, error)
|
|
AddSecret(name string, value []byte, force bool) error
|
|
GetName() string
|
|
GetFilesystem() afero.Fs
|
|
GetCurrentUnlocker() (Unlocker, error)
|
|
CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error)
|
|
}
|
|
|
|
// Secret represents a secret in a vault
|
|
type Secret struct {
|
|
Name string
|
|
Directory string
|
|
Metadata SecretMetadata
|
|
vault VaultInterface
|
|
}
|
|
|
|
// NewSecret creates a new Secret instance
|
|
func NewSecret(vault VaultInterface, name string) *Secret {
|
|
DebugWith("Creating new secret instance",
|
|
slog.String("secret_name", name),
|
|
slog.String("vault_name", vault.GetName()),
|
|
)
|
|
|
|
// 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 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 (deprecated method)",
|
|
slog.String("secret_name", s.Name),
|
|
slog.String("vault_name", s.vault.GetName()),
|
|
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 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),
|
|
slog.String("vault_name", s.vault.GetName()),
|
|
)
|
|
|
|
// 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.GetName())
|
|
return nil, fmt.Errorf("secret %s not found", 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 != "" {
|
|
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)
|
|
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)
|
|
|
|
// Use the long-term key to decrypt the version
|
|
return version.GetValue(ltIdentity)
|
|
}
|
|
|
|
Debug("Using unlocker for vault access", "secret_name", s.Name)
|
|
|
|
// Use the provided unlocker to get the vault's long-term private key
|
|
if unlocker == nil {
|
|
Debug("No unlocker provided for secret decryption", "secret_name", s.Name)
|
|
return nil, fmt.Errorf("unlocker required to decrypt secret")
|
|
}
|
|
|
|
DebugWith("Getting vault's long-term key using unlocker",
|
|
slog.String("secret_name", s.Name),
|
|
slog.String("unlocker_type", unlocker.GetType()),
|
|
slog.String("unlocker_id", unlocker.GetID()),
|
|
)
|
|
|
|
// Step 1: Use the unlocker to get the vault's long-term private key
|
|
unlockIdentity, err := unlocker.GetIdentity()
|
|
if err != nil {
|
|
Debug("Failed to get unlocker identity", "error", err, "secret_name", s.Name, "unlocker_type", unlocker.GetType())
|
|
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
|
|
}
|
|
|
|
// Read the encrypted long-term private key from the unlocker directory
|
|
encryptedLtPrivKeyPath := filepath.Join(unlocker.GetDirectory(), "longterm.age")
|
|
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
|
|
|
encryptedLtPrivKey, err := afero.ReadFile(s.vault.GetFilesystem(), 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 encrypted long-term private key using the unlocker
|
|
Debug("Decrypting long-term private key using unlocker", "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("public_key", ltIdentity.Recipient().String()),
|
|
)
|
|
|
|
// Use the long-term key to decrypt the version
|
|
return version.GetValue(ltIdentity)
|
|
}
|
|
|
|
// LoadMetadata is deprecated - metadata is now per-version and encrypted
|
|
func (s *Secret) LoadMetadata() error {
|
|
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,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetMetadata returns the secret metadata (deprecated)
|
|
func (s *Secret) GetMetadata() SecretMetadata {
|
|
Debug("GetMetadata called but is deprecated in versioned model", "secret_name", s.Name)
|
|
return s.Metadata
|
|
}
|
|
|
|
// GetEncryptedData is deprecated - data is now stored in versions
|
|
func (s *Secret) GetEncryptedData() ([]byte, error) {
|
|
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
|
|
func (s *Secret) Exists() (bool, error) {
|
|
DebugWith("Checking if secret exists",
|
|
slog.String("secret_name", s.Name),
|
|
slog.String("vault_name", s.vault.GetName()),
|
|
)
|
|
|
|
// 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 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", true),
|
|
)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// GetCurrentVault gets the current vault from the file system
|
|
// This function is a wrapper around the actual implementation in the vault package
|
|
// and exists to break the import cycle.
|
|
func GetCurrentVault(fs afero.Fs, stateDir string) (VaultInterface, error) {
|
|
// This is a forward declaration. The actual implementation is provided
|
|
// by the vault package when it calls RegisterGetCurrentVaultFunc.
|
|
if getCurrentVaultFunc == nil {
|
|
return nil, fmt.Errorf("GetCurrentVault function not registered")
|
|
}
|
|
return getCurrentVaultFunc(fs, stateDir)
|
|
}
|
|
|
|
// getCurrentVaultFunc is a function variable that will be set by the vault package
|
|
// to implement the actual GetCurrentVault functionality
|
|
var getCurrentVaultFunc func(fs afero.Fs, stateDir string) (VaultInterface, error)
|
|
|
|
// RegisterGetCurrentVaultFunc allows the vault package to register its implementation
|
|
// of GetCurrentVault to break the import cycle
|
|
func RegisterGetCurrentVaultFunc(fn func(fs afero.Fs, stateDir string) (VaultInterface, error)) {
|
|
getCurrentVaultFunc = fn
|
|
}
|