package secret import ( "errors" "log/slog" "path/filepath" "strings" "time" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" ) var ( errGetEncryptedDataDeprecated = errors.New( "GetEncryptedData is deprecated - use version-specific methods") errGetCurrentVaultNotRegistered = errors.New( "GetCurrentVault function not registered") ) // VaultInterface defines the interface that vault implementations must satisfy type VaultInterface interface { GetDirectory() (string, error) AddSecret(name string, value *memguard.LockedBuffer, force bool) error GetName() string GetFilesystem() afero.Fs GetCurrentUnlocker() (Unlocker, error) GetOrDeriveLongTermKey() (*age.X25519Identity, error) // SetMnemonic and SetUnlockPassphrase give GetOrDeriveLongTermKey the // mnemonic to derive the long-term key from, and the passphrase for a // current passphrase unlocker; nil for none. SetMnemonic(mnemonic *memguard.LockedBuffer) SetUnlockPassphrase(passphrase *memguard.LockedBuffer) CreatePassphraseUnlocker( passphrase *memguard.LockedBuffer) (*PassphraseUnlocker, error) } // Secret represents a secret in a vault type Secret struct { Name string Directory string Metadata Metadata 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: Metadata{ CreatedAt: time.Now(), UpdatedAt: time.Now(), }, } } // 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 = Metadata{ CreatedAt: now, UpdatedAt: now, } return nil } // GetMetadata returns the secret metadata (deprecated) func (s *Secret) GetMetadata() Metadata { 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, errGetEncryptedDataDeprecated } // 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. // //nolint:ireturn // must return the interface 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, errGetCurrentVaultNotRegistered } return getCurrentVaultFunc(fs, stateDir) } // getCurrentVaultFunc is a function variable that will be set by the vault package // to implement the actual GetCurrentVault functionality // //nolint:gochecknoglobals // Required to break import cycle 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 }