check / check (push) Failing after 2s
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
171 lines
4.9 KiB
Go
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
|
|
}
|