Update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 1m8s

- 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
This commit is contained in:
2026-08-07 17:27:23 +00:00
parent 6e5e0db999
commit 9ee216f629
59 changed files with 6568 additions and 4890 deletions

View File

@@ -14,13 +14,22 @@ import (
"github.com/spf13/afero"
)
// Unlocker metadata type strings.
const (
unlockerTypePassphrase = "passphrase"
unlockerTypeSecureEnclave = "secure-enclave"
)
// GetCurrentUnlocker returns the current unlocker for this vault
//
//nolint:ireturn // returns one of several concrete unlocker implementations
func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
secret.DebugWith("Getting current unlocker", slog.String("vault_name", v.Name))
vaultDir, err := v.GetDirectory()
if err != nil {
secret.Debug("Failed to get vault directory for unlocker", "error", err, "vault_name", v.Name)
secret.Debug("Failed to get vault directory for unlocker",
"error", err, "vault_name", v.Name)
return nil, err
}
@@ -30,7 +39,8 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
// Check if the symlink exists
_, err = v.fs.Stat(currentUnlockerPath)
if err != nil {
secret.Debug("Failed to stat current unlocker symlink", "error", err, "path", currentUnlockerPath)
secret.Debug("Failed to stat current unlocker symlink",
"error", err, "path", currentUnlockerPath)
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
}
@@ -47,49 +57,37 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
)
// Read unlocker metadata
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
secret.Debug("Reading unlocker metadata", "path", metadataPath)
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
metadata, err := v.readUnlockerMetadata(unlockerDir)
if err != nil {
secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
return nil, fmt.Errorf("failed to read unlocker metadata: %w", err)
return nil, err
}
var metadata UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
return nil, fmt.Errorf("failed to parse unlocker metadata: %w", err)
}
secret.DebugWith("Parsed unlocker metadata",
slog.String("unlocker_type", metadata.Type),
slog.Time("created_at", metadata.CreatedAt),
slog.Any("flags", metadata.Flags),
)
// Create unlocker instance using direct constructors with filesystem
var unlocker secret.Unlocker
// Use metadata directly as it's already the correct type
switch metadata.Type {
case "passphrase":
secret.Debug("Creating passphrase unlocker instance", "unlocker_type", metadata.Type)
case unlockerTypePassphrase:
secret.Debug("Creating passphrase unlocker instance",
"unlocker_type", metadata.Type)
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata)
case "pgp":
secret.Debug("Creating PGP unlocker instance", "unlocker_type", metadata.Type)
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, metadata)
case "keychain":
secret.Debug("Creating keychain unlocker instance", "unlocker_type", metadata.Type)
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDir, metadata)
case "secure-enclave":
secret.Debug("Creating secure enclave unlocker instance", "unlocker_type", metadata.Type)
case unlockerTypeSecureEnclave:
secret.Debug("Creating secure enclave unlocker instance",
"unlocker_type", metadata.Type)
unlocker = secret.NewSecureEnclaveUnlocker(v.fs, unlockerDir, metadata)
default:
secret.Debug("Unsupported unlocker type", "type", metadata.Type)
return nil, fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
return nil, fmt.Errorf("%w: %s", ErrUnsupportedUnlockerType, metadata.Type)
}
secret.DebugWith("Successfully created unlocker instance",
@@ -101,14 +99,16 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
return unlocker, nil
}
// resolveUnlockerDirectory reads the current-unlocker file to get the unlocker directory path
// resolveUnlockerDirectory reads the current-unlocker file to get the
// unlocker directory path
// The file contains just the unlocker name (e.g., "passphrase")
func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, error) {
secret.Debug("Reading current-unlocker file", "path", currentUnlockerPath)
unlockerNameBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
if err != nil {
secret.Debug("Failed to read current-unlocker file", "error", err, "path", currentUnlockerPath)
secret.Debug("Failed to read current-unlocker file",
"error", err, "path", currentUnlockerPath)
return "", fmt.Errorf("failed to read current unlocker: %w", err)
}
@@ -125,8 +125,13 @@ func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, er
return absolutePath, nil
}
// findUnlockerByID finds an unlocker by its ID and returns the unlocker instance and its directory path
func (v *Vault) findUnlockerByID(unlockersDir, unlockerID string) (secret.Unlocker, string, error) {
// findUnlockerByID finds an unlocker by its ID and returns the unlocker
// instance and its directory path
//
//nolint:ireturn // returns one of several concrete unlocker implementations
func (v *Vault) findUnlockerByID(
unlockersDir, unlockerID string,
) (secret.Unlocker, string, error) {
files, err := afero.ReadDir(v.fs, unlockersDir)
if err != nil {
return nil, "", fmt.Errorf("failed to read unlockers directory: %w", err)
@@ -139,10 +144,14 @@ func (v *Vault) findUnlockerByID(unlockersDir, unlockerID string) (secret.Unlock
// Read metadata file
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
exists, err := afero.Exists(v.fs, metadataPath)
if err != nil {
return nil, "", fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
return nil, "", fmt.Errorf(
"failed to check if metadata exists for unlocker %s: %w",
file.Name(), err)
}
if !exists {
// Skip directories without metadata - they might not be unlockers
continue
@@ -150,26 +159,31 @@ func (v *Vault) findUnlockerByID(unlockersDir, unlockerID string) (secret.Unlock
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
if err != nil {
return nil, "", fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
return nil, "", fmt.Errorf(
"failed to read metadata for unlocker %s: %w", file.Name(), err)
}
var metadata UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return nil, "", fmt.Errorf("failed to parse metadata for unlocker %s: %w", file.Name(), err)
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
return nil, "", fmt.Errorf(
"failed to parse metadata for unlocker %s: %w", file.Name(), err)
}
unlockerDirPath := filepath.Join(unlockersDir, file.Name())
// Create the appropriate unlocker instance
var tempUnlocker secret.Unlocker
switch metadata.Type {
case "passphrase":
case unlockerTypePassphrase:
tempUnlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, metadata)
case "pgp":
tempUnlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, metadata)
case "keychain":
tempUnlocker = secret.NewKeychainUnlocker(v.fs, unlockerDirPath, metadata)
case "secure-enclave":
case unlockerTypeSecureEnclave:
tempUnlocker = secret.NewSecureEnclaveUnlocker(v.fs, unlockerDirPath, metadata)
default:
continue
@@ -198,6 +212,7 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
if err != nil {
return nil, fmt.Errorf("failed to check if unlockers directory exists: %w", err)
}
if !exists {
return []UnlockerMetadata{}, nil
}
@@ -209,28 +224,39 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
}
var unlockers []UnlockerMetadata
for _, file := range files {
if file.IsDir() {
// Read metadata file
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
metadataPath := filepath.Join(unlockersDir, file.Name(),
"unlocker-metadata.json")
exists, err := afero.Exists(v.fs, metadataPath)
if err != nil {
return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
return nil, fmt.Errorf(
"failed to check if metadata exists for unlocker %s: %w",
file.Name(), err)
}
if !exists {
secret.Warn("Skipping unlocker directory with missing metadata file", "directory", file.Name())
secret.Warn("Skipping unlocker directory with missing metadata file",
"directory", file.Name())
continue
}
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
if err != nil {
return nil, fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
return nil, fmt.Errorf(
"failed to read metadata for unlocker %s: %w", file.Name(), err)
}
var metadata UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return nil, fmt.Errorf("failed to parse metadata for unlocker %s: %w", file.Name(), err)
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
return nil, fmt.Errorf(
"failed to parse metadata for unlocker %s: %w", file.Name(), err)
}
unlockers = append(unlockers, metadata)
@@ -257,7 +283,7 @@ func (v *Vault) RemoveUnlocker(unlockerID string) error {
}
if unlocker == nil {
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
return fmt.Errorf("%w: %s", ErrUnlockerNotFound, unlockerID)
}
// Use the unlocker's Remove method
@@ -281,17 +307,21 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
}
if targetUnlockerDir == "" {
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
return fmt.Errorf("%w: %s", ErrUnlockerNotFound, unlockerID)
}
// Create/update current-unlocker file with just the unlocker name
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
// Remove existing file if it exists
if exists, err := afero.Exists(v.fs, currentUnlockerPath); err != nil {
exists, err := afero.Exists(v.fs, currentUnlockerPath)
if err != nil {
return fmt.Errorf("failed to check if current-unlocker file exists: %w", err)
} else if exists {
if err := v.fs.Remove(currentUnlockerPath); err != nil {
}
if exists {
err = v.fs.Remove(currentUnlockerPath)
if err != nil {
return fmt.Errorf("failed to remove existing current-unlocker file: %w", err)
}
}
@@ -301,7 +331,10 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
// Write just the unlocker name to the file
secret.Debug("Writing current-unlocker file", "unlocker_name", unlockerName)
if err := afero.WriteFile(v.fs, currentUnlockerPath, []byte(unlockerName), secret.FilePerms); err != nil {
err = afero.WriteFile(v.fs, currentUnlockerPath, []byte(unlockerName),
secret.FilePerms)
if err != nil {
return fmt.Errorf("failed to create current-unlocker file: %w", err)
}
@@ -310,15 +343,19 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
// The passphrase must be provided as a LockedBuffer for security
func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*secret.PassphraseUnlocker, error) {
func (v *Vault) CreatePassphraseUnlocker(
passphrase *memguard.LockedBuffer,
) (*secret.PassphraseUnlocker, error) {
vaultDir, err := v.GetDirectory()
if err != nil {
return nil, fmt.Errorf("failed to get vault directory: %w", err)
}
// Create unlocker directory
unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
if err := v.fs.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerTypePassphrase)
err = v.fs.MkdirAll(unlockerDir, secret.DirPerms)
if err != nil {
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
}
@@ -328,32 +365,15 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
return nil, fmt.Errorf("failed to generate unlocker: %w", err)
}
// Write public key
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
if err := afero.WriteFile(v.fs, pubKeyPath,
[]byte(unlockerIdentity.Recipient().String()),
secret.FilePerms); err != nil {
return nil, fmt.Errorf("failed to write unlocker public key: %w", err)
}
// Encrypt private key with passphrase
privKeyStr := unlockerIdentity.String()
privKeyBuffer := memguard.NewBufferFromBytes([]byte(privKeyStr))
defer privKeyBuffer.Destroy()
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer, passphrase)
// Write the unlocker keypair (public and passphrase-encrypted private)
err = v.writeUnlockerKeypair(unlockerDir, unlockerIdentity, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
}
// Write encrypted private key
privKeyPath := filepath.Join(unlockerDir, "priv.age")
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
return nil, fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
return nil, err
}
// Create metadata
metadata := UnlockerMetadata{
Type: "passphrase",
Type: unlockerTypePassphrase,
CreatedAt: time.Now(),
Flags: []string{},
}
@@ -365,7 +385,9 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
}
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
err = afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms)
if err != nil {
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
}
@@ -379,13 +401,16 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
ltPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(ltIdentity.String()))
defer ltPrivKeyBuffer.Destroy()
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer, unlockerIdentity.Recipient())
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer,
unlockerIdentity.Recipient())
if err != nil {
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
}
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
err = afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms)
if err != nil {
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
}
@@ -393,9 +418,80 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
unlocker := secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata)
// Select this unlocker as current
if err := v.SelectUnlocker(unlocker.GetID()); err != nil {
err = v.SelectUnlocker(unlocker.GetID())
if err != nil {
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
}
return unlocker, nil
}
// readUnlockerMetadata reads and parses the unlocker-metadata.json file in
// the given unlocker directory.
func (v *Vault) readUnlockerMetadata(unlockerDir string) (UnlockerMetadata, error) {
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
secret.Debug("Reading unlocker metadata", "path", metadataPath)
var metadata UnlockerMetadata
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
if err != nil {
secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
return metadata, fmt.Errorf("failed to read unlocker metadata: %w", err)
}
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
return metadata, fmt.Errorf("failed to parse unlocker metadata: %w", err)
}
secret.DebugWith("Parsed unlocker metadata",
slog.String("unlocker_type", metadata.Type),
slog.Time("created_at", metadata.CreatedAt),
slog.Any("flags", metadata.Flags),
)
return metadata, nil
}
// writeUnlockerKeypair writes the unlocker's public key and its
// passphrase-encrypted private key into the unlocker directory.
func (v *Vault) writeUnlockerKeypair(
unlockerDir string,
unlockerIdentity *age.X25519Identity,
passphrase *memguard.LockedBuffer,
) error {
// Write public key
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
err := afero.WriteFile(v.fs, pubKeyPath,
[]byte(unlockerIdentity.Recipient().String()),
secret.FilePerms)
if err != nil {
return fmt.Errorf("failed to write unlocker public key: %w", err)
}
// Encrypt private key with passphrase
privKeyStr := unlockerIdentity.String()
privKeyBuffer := memguard.NewBufferFromBytes([]byte(privKeyStr))
defer privKeyBuffer.Destroy()
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer, passphrase)
if err != nil {
return fmt.Errorf("failed to encrypt unlocker private key: %w", err)
}
// Write encrypted private key
privKeyPath := filepath.Join(unlockerDir, "priv.age")
err = afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms)
if err != nil {
return fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
}
return nil
}