Update golangci-lint to v2.12.2 with canonical config (closes #30)
All checks were successful
check / check (push) Successful in 2m0s
All checks were successful
check / check (push) Successful in 2m0s
- Replace .golangci.yml with the canonical strict config (all linters enabled except the standard disable list; lll 88, funlen 80/50, cyclop 15, dupl 100; test files now linted) - Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by tag and digest (Debian-based) - Fix all ~1550 findings surfaced by the new config: line wrapping, wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel errors, perfsprint/modernize rewrites, goconst constants, thelper, testifylint, noctx CommandContext, testpackage conversions, t.Parallel() where safe, and complexity/dupl helper extraction - Record the change and follow-up items in TODO.md User-visible strings -------------------- No user-visible string changes remain. Every error message this branch composes is byte-identical to the one main composes. The err113 sentinels are shaped so that fmt.Errorf reassembles the original text around them: a sentinel carries the fixed words of the message and the caller supplies the interpolated value in the position it has always occupied. Where the value sits in the middle of the sentence the sentinel therefore holds only a fragment (for example vault.ErrVaultNotFound is "does not exist", composed by its caller as "vault <name> does not exist"); each such sentinel documents the message it participates in. Verified mechanically rather than by inspection: every fmt.Errorf and errors.New call site in both trees was parsed, the Error() text of any sentinel passed to %w substituted in, and the resulting sets of composed message templates compared. All 350 templates main produces are still produced, character for character; the set of messages lost or altered is empty. unlocker list ------------- findUnlockerIDByMetadata now returns (string, error) instead of signalling failure with an empty ID. An unreadable unlockers.d is no longer indistinguishable from "no matching entry", so UnlockersList skips the entry with a warning naming the directory, as it did before the scan was extracted into a helper, rather than emitting a row under a synthesized fallback ID that no unlocker remove or unlocker select can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by tests in internal/cli/unlockers_list_test.go.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,7 +22,8 @@ func (v *Vault) ListSecrets() ([]string, error) {
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for secret listing", "error", err, "vault_name", v.Name)
|
||||
secret.Debug("Failed to get vault directory for secret listing",
|
||||
"error", err, "vault_name", v.Name)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
@@ -31,12 +33,15 @@ func (v *Vault) ListSecrets() ([]string, error) {
|
||||
// Check if secrets directory exists
|
||||
exists, err := afero.DirExists(v.fs, secretsDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check secrets directory", "error", err, "secrets_dir", secretsDir)
|
||||
secret.Debug("Failed to check secrets directory",
|
||||
"error", err, "secrets_dir", secretsDir)
|
||||
|
||||
return nil, fmt.Errorf("failed to check if secrets directory exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
secret.Debug("Secrets directory does not exist", "secrets_dir", secretsDir, "vault_name", v.Name)
|
||||
secret.Debug("Secrets directory does not exist",
|
||||
"secrets_dir", secretsDir, "vault_name", v.Name)
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
@@ -44,12 +49,14 @@ func (v *Vault) ListSecrets() ([]string, error) {
|
||||
// List directories in secrets.d
|
||||
files, err := afero.ReadDir(v.fs, secretsDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read secrets directory", "error", err, "secrets_dir", secretsDir)
|
||||
secret.Debug("Failed to read secrets directory",
|
||||
"error", err, "secrets_dir", secretsDir)
|
||||
|
||||
return nil, fmt.Errorf("failed to read secrets directory: %w", err)
|
||||
}
|
||||
|
||||
var secrets []string
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Convert storage name back to secret name
|
||||
@@ -93,10 +100,8 @@ func isValidSecretName(name string) bool {
|
||||
}
|
||||
|
||||
// Check for path traversal via ".." components
|
||||
for _, part := range strings.Split(name, "/") {
|
||||
if part == ".." {
|
||||
return false
|
||||
}
|
||||
if slices.Contains(strings.Split(name, "/"), "..") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the basic pattern
|
||||
@@ -108,7 +113,7 @@ func isValidSecretName(name string) bool {
|
||||
// AddSecret adds a secret to this vault
|
||||
func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool) error {
|
||||
if value == nil {
|
||||
return fmt.Errorf("value buffer is nil")
|
||||
return ErrNilValueBuffer
|
||||
}
|
||||
|
||||
secret.DebugWith("Adding secret to vault",
|
||||
@@ -122,17 +127,24 @@ func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool)
|
||||
if !isValidSecretName(name) {
|
||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||
|
||||
return fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name)
|
||||
return fmt.Errorf(
|
||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
||||
ErrInvalidSecretName, name,
|
||||
)
|
||||
}
|
||||
|
||||
secret.Debug("Secret name validation passed", "secret_name", name)
|
||||
|
||||
secret.Debug("Getting vault directory")
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for secret addition", "error", err, "vault_name", v.Name)
|
||||
secret.Debug("Failed to get vault directory for secret addition",
|
||||
"error", err, "vault_name", v.Name)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
secret.Debug("Got vault directory", "vault_dir", vaultDir)
|
||||
|
||||
// Convert slashes to percent signs for storage
|
||||
@@ -144,112 +156,30 @@ func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool)
|
||||
slog.String("secret_dir", secretDir),
|
||||
)
|
||||
|
||||
// Check if secret already exists
|
||||
secret.Debug("Checking if secret already exists", "secret_dir", secretDir)
|
||||
exists, err := afero.DirExists(v.fs, secretDir)
|
||||
// Check for an existing secret and prepare its directory
|
||||
exists, previousVersion, err := v.prepareSecretDir(name, secretDir, force)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if secret exists", "error", err, "secret_dir", secretDir)
|
||||
|
||||
return fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
return err
|
||||
}
|
||||
secret.Debug("Secret existence check complete", "exists", exists)
|
||||
|
||||
// Handle existing secret case
|
||||
now := time.Now()
|
||||
var previousVersion *secret.Version
|
||||
|
||||
if exists {
|
||||
if !force {
|
||||
secret.Debug("Secret already exists and force not specified", "secret_name", name, "secret_dir", secretDir)
|
||||
|
||||
return fmt.Errorf("secret %s already exists (use --force to overwrite)", name)
|
||||
}
|
||||
|
||||
// Get the current version to update its notAfter timestamp
|
||||
currentVersionName, err := secret.GetCurrentVersion(v.fs, secretDir)
|
||||
if err == nil && currentVersionName != "" {
|
||||
previousVersion = secret.NewVersion(v, name, currentVersionName)
|
||||
// We'll need to load and update its metadata after we unlock the vault
|
||||
}
|
||||
} else {
|
||||
// Create secret directory for new secret
|
||||
secret.Debug("Creating secret directory", "secret_dir", secretDir)
|
||||
if err := v.fs.MkdirAll(secretDir, secret.DirPerms); err != nil {
|
||||
secret.Debug("Failed to create secret directory", "error", err, "secret_dir", secretDir)
|
||||
|
||||
return fmt.Errorf("failed to create secret directory: %w", err)
|
||||
}
|
||||
secret.Debug("Created secret directory successfully")
|
||||
}
|
||||
|
||||
// Generate new version name
|
||||
versionName, err := secret.GenerateVersionName(v.fs, secretDir)
|
||||
// Create the new version and save the encrypted value
|
||||
versionName, err := v.createAndSaveVersion(
|
||||
name, secretDir, value, previousVersion, &now, exists)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to generate version name", "error", err, "secret_name", name)
|
||||
|
||||
return fmt.Errorf("failed to generate version name: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
secret.Debug("Generated new version name", "version", versionName, "secret_name", name)
|
||||
|
||||
// Create new version
|
||||
newVersion := secret.NewVersion(v, name, versionName)
|
||||
|
||||
// Set version timestamps
|
||||
if previousVersion == nil {
|
||||
// First version: notBefore = epoch + 1 second
|
||||
epochPlusOne := time.Unix(1, 0)
|
||||
newVersion.Metadata.NotBefore = &epochPlusOne
|
||||
} else {
|
||||
// New version: notBefore = now
|
||||
newVersion.Metadata.NotBefore = &now
|
||||
|
||||
// We'll update the previous version's notAfter after we save the new version
|
||||
}
|
||||
|
||||
// Save the new version - pass the LockedBuffer directly
|
||||
if err := newVersion.Save(value); err != nil {
|
||||
secret.Debug("Failed to save new version", "error", err, "version", versionName)
|
||||
|
||||
// Clean up the secret directory if this was a new secret
|
||||
if !exists {
|
||||
secret.Debug("Cleaning up secret directory due to save failure", "secret_dir", secretDir)
|
||||
_ = v.fs.RemoveAll(secretDir)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to save version: %w", err)
|
||||
}
|
||||
|
||||
// Update previous version if it exists
|
||||
if previousVersion != nil {
|
||||
// Get long-term key to decrypt/encrypt metadata
|
||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get long-term key for metadata update", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||
}
|
||||
|
||||
// Load previous version metadata
|
||||
if err := previousVersion.LoadMetadata(ltIdentity); err != nil {
|
||||
secret.Debug("Failed to load previous version metadata", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
||||
}
|
||||
|
||||
// Update notAfter timestamp
|
||||
previousVersion.Metadata.NotAfter = &now
|
||||
|
||||
// Re-save the metadata (we need to implement an update method)
|
||||
if err := updateVersionMetadata(v.fs, previousVersion, ltIdentity); err != nil {
|
||||
secret.Debug("Failed to update previous version metadata", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to update previous version metadata: %w", err)
|
||||
}
|
||||
// Update previous version's notAfter timestamp if it exists
|
||||
err = v.updatePreviousVersion(previousVersion, &now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set current symlink to new version
|
||||
if err := secret.SetCurrentVersion(v.fs, secretDir, versionName); err != nil {
|
||||
err = secret.SetCurrentVersion(v.fs, secretDir, versionName)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to set current version", "error", err, "version", versionName)
|
||||
|
||||
return fmt.Errorf("failed to set current version: %w", err)
|
||||
@@ -263,9 +193,12 @@ func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool)
|
||||
}
|
||||
|
||||
// updateVersionMetadata updates the metadata of an existing version
|
||||
func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age.X25519Identity) error {
|
||||
func updateVersionMetadata(
|
||||
fs afero.Fs, version *secret.Version, ltIdentity *age.X25519Identity,
|
||||
) error {
|
||||
// Read the version's encrypted private key
|
||||
encryptedPrivKeyPath := filepath.Join(version.Directory, "priv.age")
|
||||
|
||||
encryptedPrivKey, err := afero.ReadFile(fs, encryptedPrivKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read encrypted version private key: %w", err)
|
||||
@@ -294,14 +227,17 @@ func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age
|
||||
metadataBuffer := memguard.NewBufferFromBytes(metadataBytes)
|
||||
defer metadataBuffer.Destroy()
|
||||
|
||||
encryptedMetadata, err := secret.EncryptToRecipient(metadataBuffer, versionIdentity.Recipient())
|
||||
encryptedMetadata, err := secret.EncryptToRecipient(metadataBuffer,
|
||||
versionIdentity.Recipient())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt version metadata: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted metadata
|
||||
metadataPath := filepath.Join(version.Directory, "metadata.age")
|
||||
if err := afero.WriteFile(fs, metadataPath, encryptedMetadata, secret.FilePerms); err != nil {
|
||||
|
||||
err = afero.WriteFile(fs, metadataPath, encryptedMetadata, secret.FilePerms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write encrypted version metadata: %w", err)
|
||||
}
|
||||
|
||||
@@ -318,7 +254,8 @@ func (v *Vault) GetSecret(name string) ([]byte, error) {
|
||||
return v.GetSecretVersion(name, "")
|
||||
}
|
||||
|
||||
// GetSecretVersion retrieves a specific version of a secret (empty version means current)
|
||||
// GetSecretVersion retrieves a specific version of a secret (empty version
|
||||
// means current)
|
||||
func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
|
||||
secret.DebugWith("Getting secret version from vault",
|
||||
slog.String("vault_name", v.Name),
|
||||
@@ -326,69 +263,17 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
|
||||
slog.String("version", version),
|
||||
)
|
||||
|
||||
// Validate secret name to prevent path traversal
|
||||
if !isValidSecretName(name) {
|
||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||
|
||||
return nil, fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name)
|
||||
}
|
||||
|
||||
// Get vault directory
|
||||
vaultDir, err := v.GetDirectory()
|
||||
// Validate the name and resolve the version to fetch
|
||||
version, err := v.resolveSecretVersion(name, version)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory", "error", err, "vault_name", v.Name)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert slashes to percent signs for storage
|
||||
storageName := strings.ReplaceAll(name, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
||||
|
||||
// Check if secret exists
|
||||
exists, err := afero.DirExists(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if secret exists", "error", err, "secret_name", name)
|
||||
|
||||
return nil, fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
secret.Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
|
||||
|
||||
return nil, fmt.Errorf("secret %s not found", name)
|
||||
}
|
||||
|
||||
// Determine which version to get
|
||||
if version == "" {
|
||||
// Get current version
|
||||
currentVersion, err := secret.GetCurrentVersion(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get current version", "error", err, "secret_name", name)
|
||||
|
||||
return nil, fmt.Errorf("failed to get current version: %w", err)
|
||||
}
|
||||
version = currentVersion
|
||||
secret.Debug("Using current version", "version", version, "secret_name", name)
|
||||
}
|
||||
|
||||
// Create version object
|
||||
secretVersion := secret.NewVersion(v, name, version)
|
||||
|
||||
// Check if version exists
|
||||
versionPath := filepath.Join(secretDir, "versions", version)
|
||||
exists, err = afero.DirExists(v.fs, versionPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if version exists", "error", err, "version", version)
|
||||
|
||||
return nil, fmt.Errorf("failed to check if version exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
secret.Debug("Version not found", "version", version, "secret_name", name)
|
||||
|
||||
return nil, fmt.Errorf("version %s not found for secret %s", version, name)
|
||||
}
|
||||
|
||||
secret.Debug("Version exists, proceeding with vault unlock and decryption", "version", version, "secret_name", name)
|
||||
secret.Debug("Version exists, proceeding with vault unlock and decryption",
|
||||
"version", version, "secret_name", name)
|
||||
|
||||
// Unlock the vault (get long-term key in memory)
|
||||
longTermIdentity, err := v.UnlockVault()
|
||||
@@ -406,10 +291,13 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
|
||||
)
|
||||
|
||||
// Get the version's value
|
||||
secret.Debug("About to call secretVersion.GetValue", "version", version, "secret_name", name)
|
||||
secret.Debug("About to call secretVersion.GetValue",
|
||||
"version", version, "secret_name", name)
|
||||
|
||||
decryptedValue, err := secretVersion.GetValue(longTermIdentity)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to decrypt version value", "error", err, "version", version, "secret_name", name)
|
||||
secret.Debug("Failed to decrypt version value",
|
||||
"error", err, "version", version, "secret_name", name)
|
||||
|
||||
return nil, fmt.Errorf("failed to decrypt version: %w", err)
|
||||
}
|
||||
@@ -442,7 +330,8 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
|
||||
|
||||
// If vault is already unlocked, return the cached key
|
||||
if !v.Locked() {
|
||||
secret.Debug("Vault already unlocked, returning cached long-term key", "vault_name", v.Name)
|
||||
secret.Debug("Vault already unlocked, returning cached long-term key",
|
||||
"vault_name", v.Name)
|
||||
|
||||
return v.longTermKey, nil
|
||||
}
|
||||
@@ -450,7 +339,8 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
|
||||
// Get or derive the long-term key (but don't store it yet)
|
||||
longTermIdentity, err := v.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get or derive long-term key", "error", err, "vault_name", v.Name)
|
||||
secret.Debug("Failed to get or derive long-term key",
|
||||
"error", err, "vault_name", v.Name)
|
||||
|
||||
return nil, fmt.Errorf("failed to get long-term key: %w", err)
|
||||
}
|
||||
@@ -469,7 +359,7 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
|
||||
// GetSecretObject retrieves a Secret object with metadata loaded from this vault
|
||||
func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
||||
if !isValidSecretName(name) {
|
||||
return nil, fmt.Errorf("invalid secret name: %s", name)
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidSecretName, name)
|
||||
}
|
||||
|
||||
// First check if the secret exists by checking for the metadata file
|
||||
@@ -487,15 +377,17 @@ func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("secret %s not found", name)
|
||||
return nil, fmt.Errorf("secret %s %w", name, ErrSecretNotFound)
|
||||
}
|
||||
|
||||
// Create a Secret object
|
||||
secretObj := secret.NewSecret(v, name)
|
||||
|
||||
// Load the metadata from disk
|
||||
if err := secretObj.LoadMetadata(); err != nil {
|
||||
err = secretObj.LoadMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -526,7 +418,8 @@ func (v *Vault) CopySecretVersion(
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
// Load source metadata
|
||||
if err := srcVersion.LoadMetadata(srcIdentity); err != nil {
|
||||
err = srcVersion.LoadMetadata(srcIdentity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load source metadata: %w", err)
|
||||
}
|
||||
|
||||
@@ -537,7 +430,8 @@ func (v *Vault) CopySecretVersion(
|
||||
destVersion.Metadata = srcVersion.Metadata
|
||||
|
||||
// Save the version (encrypts to this vault's LT key)
|
||||
if err := destVersion.Save(valueBuffer); err != nil {
|
||||
err = destVersion.Save(valueBuffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save destination version: %w", err)
|
||||
}
|
||||
|
||||
@@ -571,26 +465,13 @@ func (v *Vault) CopySecretAllVersions(
|
||||
return fmt.Errorf("failed to get destination vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if destination secret already exists
|
||||
// Check if destination secret already exists and clear it if forced
|
||||
destStorageName := strings.ReplaceAll(destSecretName, "/", "%")
|
||||
destSecretDir := filepath.Join(destVaultDir, "secrets.d", destStorageName)
|
||||
|
||||
exists, err := afero.DirExists(v.fs, destSecretDir)
|
||||
err = v.prepareCopyDestination(destSecretDir, destSecretName, force)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check destination: %w", err)
|
||||
}
|
||||
|
||||
if exists && !force {
|
||||
return fmt.Errorf("secret '%s' already exists in vault '%s' (use --force to overwrite)",
|
||||
destSecretName, v.Name)
|
||||
}
|
||||
|
||||
if exists && force {
|
||||
// Remove existing secret
|
||||
secret.Debug("Removing existing destination secret", "path", destSecretDir)
|
||||
if err := v.fs.RemoveAll(destSecretDir); err != nil {
|
||||
return fmt.Errorf("failed to remove existing destination secret: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get source vault's long-term key
|
||||
@@ -615,7 +496,7 @@ func (v *Vault) CopySecretAllVersions(
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
return fmt.Errorf("source secret '%s' has no versions", srcSecretName)
|
||||
return fmt.Errorf("source secret '%s' %w", srcSecretName, ErrNoVersions)
|
||||
}
|
||||
|
||||
// Get current version name
|
||||
@@ -625,27 +506,16 @@ func (v *Vault) CopySecretAllVersions(
|
||||
}
|
||||
|
||||
// Create destination secret directory
|
||||
if err := v.fs.MkdirAll(destSecretDir, secret.DirPerms); err != nil {
|
||||
err = v.fs.MkdirAll(destSecretDir, secret.DirPerms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination secret directory: %w", err)
|
||||
}
|
||||
|
||||
// Copy each version
|
||||
for _, versionName := range versions {
|
||||
srcVersion := secret.NewVersion(srcVault, srcSecretName, versionName)
|
||||
if err := v.CopySecretVersion(srcVersion, srcIdentity, destSecretName, versionName); err != nil {
|
||||
// Rollback: remove partial copy
|
||||
secret.Debug("Rolling back partial copy due to error", "error", err)
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to copy version %s: %w", versionName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set current version
|
||||
if err := secret.SetCurrentVersion(v.fs, destSecretDir, currentVersion); err != nil {
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to set current version: %w", err)
|
||||
// Copy each version and set the current pointer, rolling back on error
|
||||
err = v.copyVersionsWithRollback(srcVault, srcIdentity,
|
||||
srcSecretName, destSecretName, destSecretDir, versions, currentVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully copied all secret versions",
|
||||
@@ -656,3 +526,292 @@ func (v *Vault) CopySecretAllVersions(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareSecretDir checks for an existing secret directory and prepares it
|
||||
// for a new version. It returns whether the secret already existed and the
|
||||
// current version to be superseded, if any.
|
||||
func (v *Vault) prepareSecretDir(
|
||||
name, secretDir string, force bool,
|
||||
) (bool, *secret.Version, error) {
|
||||
// Check if secret already exists
|
||||
secret.Debug("Checking if secret already exists", "secret_dir", secretDir)
|
||||
|
||||
exists, err := afero.DirExists(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if secret exists",
|
||||
"error", err, "secret_dir", secretDir)
|
||||
|
||||
return false, nil, fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
|
||||
secret.Debug("Secret existence check complete", "exists", exists)
|
||||
|
||||
if !exists {
|
||||
// Create secret directory for new secret
|
||||
secret.Debug("Creating secret directory", "secret_dir", secretDir)
|
||||
|
||||
err = v.fs.MkdirAll(secretDir, secret.DirPerms)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to create secret directory",
|
||||
"error", err, "secret_dir", secretDir)
|
||||
|
||||
return false, nil, fmt.Errorf("failed to create secret directory: %w", err)
|
||||
}
|
||||
|
||||
secret.Debug("Created secret directory successfully")
|
||||
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if !force {
|
||||
secret.Debug("Secret already exists and force not specified",
|
||||
"secret_name", name, "secret_dir", secretDir)
|
||||
|
||||
return true, nil, fmt.Errorf(
|
||||
"secret %s %w (use --force to overwrite)",
|
||||
name, ErrSecretExists,
|
||||
)
|
||||
}
|
||||
|
||||
// Get the current version to update its notAfter timestamp
|
||||
var previousVersion *secret.Version
|
||||
|
||||
currentVersionName, err := secret.GetCurrentVersion(v.fs, secretDir)
|
||||
if err == nil && currentVersionName != "" {
|
||||
previousVersion = secret.NewVersion(v, name, currentVersionName)
|
||||
// We'll need to load and update its metadata after we unlock the vault
|
||||
}
|
||||
|
||||
return true, previousVersion, nil
|
||||
}
|
||||
|
||||
// updatePreviousVersion sets the notAfter timestamp on the version being
|
||||
// superseded. It is a no-op when previousVersion is nil.
|
||||
func (v *Vault) updatePreviousVersion(
|
||||
previousVersion *secret.Version, now *time.Time,
|
||||
) error {
|
||||
if previousVersion == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get long-term key to decrypt/encrypt metadata
|
||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get long-term key for metadata update", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||
}
|
||||
|
||||
// Load previous version metadata
|
||||
err = previousVersion.LoadMetadata(ltIdentity)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to load previous version metadata", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
||||
}
|
||||
|
||||
// Update notAfter timestamp
|
||||
previousVersion.Metadata.NotAfter = now
|
||||
|
||||
// Re-save the metadata (we need to implement an update method)
|
||||
err = updateVersionMetadata(v.fs, previousVersion, ltIdentity)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to update previous version metadata", "error", err)
|
||||
|
||||
return fmt.Errorf("failed to update previous version metadata: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveSecretVersion validates the secret name, verifies the secret and
|
||||
// version exist, and resolves an empty version to the current one.
|
||||
func (v *Vault) resolveSecretVersion(name, version string) (string, error) {
|
||||
// Validate secret name to prevent path traversal
|
||||
if !isValidSecretName(name) {
|
||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
||||
ErrInvalidSecretName, name,
|
||||
)
|
||||
}
|
||||
|
||||
// Get vault directory
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory", "error", err, "vault_name", v.Name)
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert slashes to percent signs for storage
|
||||
storageName := strings.ReplaceAll(name, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
||||
|
||||
// Check if secret exists
|
||||
exists, err := afero.DirExists(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if secret exists", "error", err, "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
secret.Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
|
||||
|
||||
return "", fmt.Errorf("secret %s %w", name, ErrSecretNotFound)
|
||||
}
|
||||
|
||||
// Determine which version to get
|
||||
if version == "" {
|
||||
// Get current version
|
||||
currentVersion, err := secret.GetCurrentVersion(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get current version", "error", err, "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf("failed to get current version: %w", err)
|
||||
}
|
||||
|
||||
version = currentVersion
|
||||
|
||||
secret.Debug("Using current version", "version", version, "secret_name", name)
|
||||
}
|
||||
|
||||
// Check if version exists
|
||||
versionPath := filepath.Join(secretDir, "versions", version)
|
||||
|
||||
exists, err = afero.DirExists(v.fs, versionPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if version exists", "error", err, "version", version)
|
||||
|
||||
return "", fmt.Errorf("failed to check if version exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
secret.Debug("Version not found", "version", version, "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"version %s %w %s",
|
||||
version, ErrVersionNotFound, name,
|
||||
)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// createAndSaveVersion generates a new version name, sets the version
|
||||
// timestamps, and saves the encrypted value. When saving fails for a newly
|
||||
// created secret, the secret directory is removed again.
|
||||
func (v *Vault) createAndSaveVersion(
|
||||
name, secretDir string, value *memguard.LockedBuffer,
|
||||
previousVersion *secret.Version, now *time.Time, exists bool,
|
||||
) (string, error) {
|
||||
// Generate new version name
|
||||
versionName, err := secret.GenerateVersionName(v.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to generate version name", "error", err, "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf("failed to generate version name: %w", err)
|
||||
}
|
||||
|
||||
secret.Debug("Generated new version name", "version", versionName, "secret_name", name)
|
||||
|
||||
// Create new version
|
||||
newVersion := secret.NewVersion(v, name, versionName)
|
||||
|
||||
// Set version timestamps
|
||||
if previousVersion == nil {
|
||||
// First version: notBefore = epoch + 1 second
|
||||
epochPlusOne := time.Unix(1, 0)
|
||||
newVersion.Metadata.NotBefore = &epochPlusOne
|
||||
} else {
|
||||
// New version: notBefore = now
|
||||
newVersion.Metadata.NotBefore = now
|
||||
|
||||
// We'll update the previous version's notAfter after we save the
|
||||
// new version
|
||||
}
|
||||
|
||||
// Save the new version - pass the LockedBuffer directly
|
||||
err = newVersion.Save(value)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to save new version", "error", err, "version", versionName)
|
||||
|
||||
// Clean up the secret directory if this was a new secret
|
||||
if !exists {
|
||||
secret.Debug("Cleaning up secret directory due to save failure",
|
||||
"secret_dir", secretDir)
|
||||
|
||||
_ = v.fs.RemoveAll(secretDir)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to save version: %w", err)
|
||||
}
|
||||
|
||||
return versionName, nil
|
||||
}
|
||||
|
||||
// copyVersionsWithRollback copies each version of the source secret into the
|
||||
// destination directory and sets the current version pointer, removing the
|
||||
// partial copy when any step fails.
|
||||
func (v *Vault) copyVersionsWithRollback(
|
||||
srcVault *Vault, srcIdentity *age.X25519Identity,
|
||||
srcSecretName, destSecretName, destSecretDir string,
|
||||
versions []string, currentVersion string,
|
||||
) error {
|
||||
// Copy each version
|
||||
for _, versionName := range versions {
|
||||
srcVersion := secret.NewVersion(srcVault, srcSecretName, versionName)
|
||||
|
||||
err := v.CopySecretVersion(srcVersion, srcIdentity, destSecretName, versionName)
|
||||
if err != nil {
|
||||
// Rollback: remove partial copy
|
||||
secret.Debug("Rolling back partial copy due to error", "error", err)
|
||||
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to copy version %s: %w", versionName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set current version
|
||||
err := secret.SetCurrentVersion(v.fs, destSecretDir, currentVersion)
|
||||
if err != nil {
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to set current version: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareCopyDestination ensures the destination secret directory can be
|
||||
// created, removing an existing secret when force is set.
|
||||
func (v *Vault) prepareCopyDestination(
|
||||
destSecretDir, destSecretName string, force bool,
|
||||
) error {
|
||||
exists, err := afero.DirExists(v.fs, destSecretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check destination: %w", err)
|
||||
}
|
||||
|
||||
if exists && !force {
|
||||
return fmt.Errorf(
|
||||
"secret '%s' %w in vault '%s' (use --force to overwrite)",
|
||||
destSecretName, ErrSecretExists, v.Name,
|
||||
)
|
||||
}
|
||||
|
||||
if exists && force {
|
||||
// Remove existing secret
|
||||
secret.Debug("Removing existing destination secret", "path", destSecretDir)
|
||||
|
||||
err = v.fs.RemoveAll(destSecretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing destination secret: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user