fix: resolve all nestif linter errors

- Extract getLongTermPrivateKey helper function to reduce nesting in keychainunlocker.go and pgpunlocker.go
- Add getPassphrase helper method to reduce nesting in passphraseunlocker.go
- Refactor version serial extraction to use early returns in version.go
- Extract resolveRelativeSymlink and tryResolveOsSymlink helpers in management.go
- Add processMnemonicForVault helper to reduce nesting in vault creation
- Extract resolveUnlockerDirectory and readUnlockerPathFromFile helpers in unlockers.go
- Add findUnlockerByID helper to reduce duplicate code in RemoveUnlocker and SelectUnlocker

All tests pass after refactoring.
This commit is contained in:
2025-07-15 05:47:16 +02:00
parent 4e242c3491
commit 811ddee3b7
6 changed files with 330 additions and 356 deletions

View File

@@ -220,6 +220,68 @@ func generateKeychainUnlockerName(vaultName string) (string, error) {
return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
}
// getLongTermPrivateKey retrieves the long-term private key either from environment or current unlocker
func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) ([]byte, error) {
// Check if mnemonic is available in environment variable
envMnemonic := os.Getenv(EnvMnemonic)
if envMnemonic != "" {
// Use mnemonic directly to derive long-term key
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
return []byte(ltIdentity.String()), nil
}
// Get the vault to access current unlocker
currentUnlocker, err := vault.GetCurrentUnlocker()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
}
// Get the current unlocker identity
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
}
// Get encrypted long-term key from current unlocker, handling different types
var encryptedLtPrivKey []byte
switch currentUnlocker := currentUnlocker.(type) {
case *PassphraseUnlocker:
// Read the encrypted long-term private key from passphrase unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
}
case *PGPUnlocker:
// Read the encrypted long-term private key from PGP unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
}
case *KeychainUnlocker:
// Read the encrypted long-term private key from another keychain unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlocker: %w", err)
}
default:
return nil, fmt.Errorf("unsupported current unlocker type for keychain unlocker creation")
}
// Decrypt long-term private key using current unlocker
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
return ltPrivKeyData, nil
}
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
// Check if we're on macOS
@@ -282,62 +344,9 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
}
// Step 5: Get or derive the long-term private key
var ltPrivKeyData []byte
// Check if mnemonic is available in environment variable
if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
// Use mnemonic directly to derive long-term key
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
ltPrivKeyData = []byte(ltIdentity.String())
} else {
// Get the vault to access current unlocker
currentUnlocker, err := vault.GetCurrentUnlocker()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
}
// Get the current unlocker identity
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
}
// Get encrypted long-term key from current unlocker, handling different types
var encryptedLtPrivKey []byte
switch currentUnlocker := currentUnlocker.(type) {
case *PassphraseUnlocker:
// Read the encrypted long-term private key from passphrase unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
}
case *PGPUnlocker:
// Read the encrypted long-term private key from PGP unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
}
case *KeychainUnlocker:
// Read the encrypted long-term private key from another keychain unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlocker: %w", err)
}
default:
return nil, fmt.Errorf("unsupported current unlocker type for keychain unlocker creation")
}
// Decrypt long-term private key using current unlocker
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
ltPrivKeyData, err := getLongTermPrivateKey(fs, vault)
if err != nil {
return nil, err
}
// Step 6: Encrypt long-term private key to the new age unlocker

View File

@@ -18,6 +18,32 @@ type PassphraseUnlocker struct {
Passphrase string
}
// getPassphrase retrieves the passphrase from memory, environment, or user input
func (p *PassphraseUnlocker) getPassphrase() (string, error) {
// First check if we already have the passphrase
if p.Passphrase != "" {
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
return p.Passphrase, nil
}
Debug("No passphrase in memory, checking environment")
// Check environment variable for passphrase
passphraseStr := os.Getenv(EnvUnlockPassphrase)
if passphraseStr != "" {
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
return passphraseStr, nil
}
Debug("No passphrase in environment, prompting user")
// Prompt for passphrase
passphraseStr, err := ReadPassphrase("Enter unlock passphrase: ")
if err != nil {
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
return "", fmt.Errorf("failed to read passphrase: %w", err)
}
return passphraseStr, nil
}
// GetIdentity implements Unlocker interface for passphrase-based unlockers
func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
DebugWith("Getting passphrase unlocker identity",
@@ -25,26 +51,9 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
slog.String("unlocker_type", p.GetType()),
)
// First check if we already have the passphrase
passphraseStr := p.Passphrase
if passphraseStr == "" {
Debug("No passphrase in memory, checking environment")
// Check environment variable for passphrase
passphraseStr = os.Getenv(EnvUnlockPassphrase)
if passphraseStr == "" {
Debug("No passphrase in environment, prompting user")
// Prompt for passphrase
var err error
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
if err != nil {
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
return nil, fmt.Errorf("failed to read passphrase: %w", err)
}
} else {
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
}
} else {
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
passphraseStr, err := p.getPassphrase()
if err != nil {
return nil, err
}
// Read encrypted private key of unlocker

View File

@@ -12,7 +12,6 @@ import (
"time"
"filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/spf13/afero"
)
@@ -227,55 +226,9 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
}
// Step 3: Get or derive the long-term private key
var ltPrivKeyData []byte
// Check if mnemonic is available in environment variable
if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
// Use mnemonic directly to derive long-term key
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
ltPrivKeyData = []byte(ltIdentity.String())
} else {
// Get the vault to access current unlocker
currentUnlocker, err := vault.GetCurrentUnlocker()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
}
// Get the current unlocker identity
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
if err != nil {
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
}
// Get encrypted long-term key from current unlocker, handling different types
var encryptedLtPrivKey []byte
switch currentUnlocker := currentUnlocker.(type) {
case *PassphraseUnlocker:
// Read the encrypted long-term private key from passphrase unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
}
case *PGPUnlocker:
// Read the encrypted long-term private key from PGP unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
if err != nil {
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
}
default:
return nil, fmt.Errorf("unsupported current unlocker type for PGP unlocker creation")
}
// Step 6: Decrypt long-term private key using current unlocker
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
ltPrivKeyData, err := getLongTermPrivateKey(fs, vault)
if err != nil {
return nil, err
}
// Step 7: Encrypt long-term private key to the new age unlocker

View File

@@ -89,17 +89,24 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
prefix := today + "."
for _, entry := range entries {
if entry.IsDir() && strings.HasPrefix(entry.Name(), prefix) {
// Extract serial number
parts := strings.Split(entry.Name(), ".")
if len(parts) == versionNameParts {
var serial int
if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err == nil {
if serial > maxSerial {
maxSerial = serial
}
}
}
// Skip non-directories and those without correct prefix
if !entry.IsDir() || !strings.HasPrefix(entry.Name(), prefix) {
continue
}
// Extract serial number
parts := strings.Split(entry.Name(), ".")
if len(parts) != versionNameParts {
continue
}
var serial int
if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err != nil {
continue
}
if serial > maxSerial {
maxSerial = serial
}
}