Give each failure one error value (closes #113)
check / check (push) Failing after 3s

internal/cli drops its copies of vault.ErrSecretNotFound, ErrVaultNotFound,
ErrVersionNotFound and ErrSecretExists and of the secret package's keychain
and Secure Enclave errors, and its second error for an unknown unlocker
type, an invalid mnemonic, a length below 1, an unsupported secret type and
an oversized secret. vault.ErrNilValueBuffer becomes
secret.ErrNilValueBuffer. Every error of secret.ReadPassphrase wraps
ErrPassphraseNotRead. ResolveGPGKeyFingerprint returns ErrGPGKeyNotFound for
a key the keyring lacks. storeInKeychain returns errNilDataBuffer. bip85's
ErrPasswordTooShort and ErrEncodedTooShort go with their unreachable checks,
as does the macOS check in macOS-only code. Tests that matched these
errors' text use errors.Is.

Model: opus-5-5
This commit was merged in pull request #114.
This commit is contained in:
2026-10-05 01:08:01 +02:00
parent 176095e3d1
commit 43f66bf369
28 changed files with 271 additions and 162 deletions
+6 -5
View File
@@ -166,19 +166,20 @@ func DecryptWithPassphrase(
// ReadPassphrase reads a passphrase securely from the terminal without echoing
// This version is for unlocking and doesn't require confirmation
// Returns a LockedBuffer containing the passphrase for secure memory handling
// Returns a LockedBuffer containing the passphrase for secure memory handling.
// Every error it returns wraps ErrPassphraseNotRead.
func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
// Check if stdin is a terminal
if !term.IsTerminal(syscall.Stdin) {
// Not a terminal - never read passphrases from piped input
// for security reasons
return nil, errStdinNotTerminal
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, errStdinNotTerminal)
}
// stdin is a terminal, check if stderr is also a terminal for
// interactive prompting
if !term.IsTerminal(syscall.Stderr) {
return nil, errStderrNotTerminal
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, errStderrNotTerminal)
}
// Both stdin and stderr are terminals - use secure password reading
@@ -186,14 +187,14 @@ func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
passphrase, err := term.ReadPassword(syscall.Stdin)
if err != nil {
return nil, fmt.Errorf("failed to read passphrase: %w", err)
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, err)
}
// Print newline to stderr since ReadPassword doesn't echo
fmt.Fprintln(os.Stderr)
if len(passphrase) == 0 {
return nil, errEmptyPassphrase
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, errEmptyPassphrase)
}
// Create a secure buffer and copy the passphrase
-18
View File
@@ -11,7 +11,6 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"time"
"filippo.io/age"
@@ -39,8 +38,6 @@ const (
var keychainItemNameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
var (
errNotMacOS = errors.New(
"keychain unlockers are only supported on macOS")
errKeychainItemNameEmpty = errors.New("keychain item name cannot be empty")
errInvalidKeychainItemName = errors.New("invalid keychain item name format")
errUnsupportedCurrentUnlocker = errors.New(
@@ -394,12 +391,6 @@ func deriveLongTermPrivateKey(
func CreateKeychainUnlocker(
fs afero.Fs, stateDir string, mnemonic, passphrase *memguard.LockedBuffer,
) (*KeychainUnlocker, error) {
// Check if we're on macOS
err := checkMacOSAvailable()
if err != nil {
return nil, err
}
// Get current vault using the GetCurrentVault function from the same package
vault, err := GetCurrentVault(fs, stateDir)
if err != nil {
@@ -555,15 +546,6 @@ func writeKeychainUnlocker(
}, nil
}
// checkMacOSAvailable verifies that we're running on macOS
func checkMacOSAvailable() error {
if runtime.GOOS != "darwin" {
return fmt.Errorf("%w, current OS: %s", errNotMacOS, runtime.GOOS)
}
return nil
}
// validateKeychainItemName validates that a keychain item name is safe for
// command execution
func validateKeychainItemName(itemName string) error {
+1 -1
View File
@@ -15,7 +15,7 @@ import (
// storeInKeychain stores data in the macOS keychain using keybase/go-keychain
func storeInKeychain(itemName string, data *memguard.LockedBuffer) error {
if data == nil {
return fmt.Errorf("data buffer is nil")
return errNilDataBuffer
}
if err := validateKeychainItemName(itemName); err != nil {
return fmt.Errorf("invalid keychain item name: %w", err)
+1 -2
View File
@@ -130,8 +130,7 @@ func TestKeychainNilData(t *testing.T) {
// Test storing nil data
err := storeInKeychain("test-item", nil)
assert.Error(t, err, "Expected error when storing nil data")
assert.Contains(t, err.Error(), "data buffer is nil")
require.ErrorIs(t, err, errNilDataBuffer)
}
func TestKeychainLargeData(t *testing.T) {
+4 -4
View File
@@ -11,9 +11,9 @@ import (
"github.com/spf13/afero"
)
// ErrPassphraseNotRead is wrapped in the error of a passphrase unlocker
// that could not read its passphrase from the terminal, for example because
// there is none. The unlocker itself was not tried.
// ErrPassphraseNotRead is wrapped in every error of ReadPassphrase: there
// is no terminal to read the passphrase from, reading it failed, or it was
// empty. A passphrase unlocker that fails with it was not tried.
var ErrPassphraseNotRead = errors.New("failed to read passphrase")
// PassphraseUnlocker represents a passphrase-protected unlocker
@@ -155,7 +155,7 @@ func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) {
if err != nil {
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, err)
return nil, err
}
return secureBuffer, nil
+16 -2
View File
@@ -18,6 +18,10 @@ import (
"github.com/spf13/afero"
)
// gpgNoPublicKeyStatus is the status line gpg writes when it has no key for
// the ID it was asked to list: 9 is gpg's error code for "No public key".
const gpgNoPublicKeyStatus = "[GNUPG:] ERROR keylist.getkey 9\n"
var (
errGPGKeyIDEmpty = errors.New("GPG key ID cannot be empty")
errInvalidGPGKeyID = errors.New("invalid GPG key ID format")
@@ -25,6 +29,10 @@ var (
errNilDataBuffer = errors.New("data buffer is nil")
)
// ErrGPGKeyNotFound is returned by ResolveGPGKeyFingerprint for a key ID
// that matches no key in the GPG keyring.
var ErrGPGKeyNotFound = errors.New("GPG key not found")
// Variables to allow overriding in tests
var (
// GPGEncryptFunc is the function used for GPG encryption
@@ -367,14 +375,20 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) {
return "", fmt.Errorf("invalid GPG key ID: %w", err)
}
// Use GPG to get the full fingerprint for the key
// Use GPG to get the full fingerprint for the key. --status-fd 1 adds
// gpg's status lines to the output.
cmd := exec.CommandContext( //nolint:gosec // G204: keyID validated above
context.Background(),
"gpg", "--list-keys", "--with-colons", "--fingerprint", keyID,
"gpg", "--status-fd", "1",
"--list-keys", "--with-colons", "--fingerprint", keyID,
)
output, err := cmd.Output()
if err != nil {
if strings.Contains(string(output), gpgNoPublicKeyStatus) {
return "", fmt.Errorf("%w: %s", ErrGPGKeyNotFound, keyID)
}
return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
}
-5
View File
@@ -216,11 +216,6 @@ func CreateSecureEnclaveUnlocker(
stateDir string,
mnemonic, passphrase *memguard.LockedBuffer,
) (*SecureEnclaveUnlocker, error) {
err := checkMacOSAvailable()
if err != nil {
return nil, err
}
vault, err := GetCurrentVault(fs, stateDir)
if err != nil {
return nil, fmt.Errorf("failed to get current vault: %w", err)
+5 -5
View File
@@ -22,10 +22,10 @@ const (
maxVersionsPerDay = 999
)
var (
errMaxVersionsPerDay = errors.New("exceeded maximum versions per day (999)")
errNilValueBuffer = errors.New("value buffer is nil")
)
var errMaxVersionsPerDay = errors.New("exceeded maximum versions per day (999)")
// ErrNilValueBuffer is returned when a secret's value is given as nil.
var ErrNilValueBuffer = errors.New("value buffer is nil")
// VersionMetadata contains information about a secret version
type VersionMetadata struct {
@@ -138,7 +138,7 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
// process dies part-way.
func (sv *Version) Save(value *memguard.LockedBuffer) error {
if value == nil {
return errNilValueBuffer
return ErrNilValueBuffer
}
DebugWith("Saving secret version",