Files
secret/internal/secret/secret.go
T
clawbot 2adc588ace
check / check (push) Failing after 2s
Say the mnemonic still opens a vault its unlocker cannot (closes #47)
When a vault cannot be opened through its current unlocker, the error now
ends by naming the vault, saying that it still opens with its mnemonic,
and that 'secret unlocker add passphrase' run with SB_SECRET_MNEMONIC set
gives it a new unlocker, after 'secret vault select' when it is not the
current vault. Only when the vault metadata records the key the mnemonic
derives, and not when the passphrase could not be read. 'secret encrypt'
and 'secret decrypt' read the key secret through vault.GetSecret, and
Secret.GetValue with its helpers is removed. An unreadable 'current'
file's error names 'secret version list' and 'secret version promote'.
Causes stay wrapped.

Model: opus-5-5
2026-10-04 21:59:02 +02:00

171 lines
4.9 KiB
Go

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
}