diff --git a/internal/secret/pgpunlocker.go b/internal/secret/pgpunlocker.go index e80ed33..ba5130d 100644 --- a/internal/secret/pgpunlocker.go +++ b/internal/secret/pgpunlocker.go @@ -388,7 +388,8 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) { return "", fmt.Errorf("%w: %s", ErrGPGKeyNotFound, keyID) } - return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err) + return "", fmt.Errorf( + "failed to resolve GPG key fingerprint: %w", withGPGStderr(err)) } // Parse the output to extract the fingerprint @@ -418,6 +419,18 @@ func checkGPGAvailable() error { return nil } +// withGPGStderr appends gpg's stderr, which says why gpg failed, to err +// from cmd.Output. cmd.Output keeps that stderr in the *exec.ExitError. +func withGPGStderr(err error) error { + var exitErr *exec.ExitError + + if errors.As(err, &exitErr) { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(exitErr.Stderr))) + } + + return err +} + // gpgEncryptDefault is the default implementation of GPG encryption func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error) { if data == nil { @@ -437,7 +450,7 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error output, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("GPG encryption failed: %w", err) + return nil, fmt.Errorf("GPG encryption failed: %w", withGPGStderr(err)) } return output, nil @@ -450,7 +463,7 @@ func gpgDecryptDefault(encryptedData []byte) (*memguard.LockedBuffer, error) { output, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("GPG decryption failed: %w", err) + return nil, fmt.Errorf("GPG decryption failed: %w", withGPGStderr(err)) } // Create a secure buffer for the decrypted data diff --git a/internal/secret/pgpunlocker_test.go b/internal/secret/pgpunlocker_test.go index 60f13c8..9fe1ee6 100644 --- a/internal/secret/pgpunlocker_test.go +++ b/internal/secret/pgpunlocker_test.go @@ -102,3 +102,16 @@ func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) { assert.NotEqual(t, first.GetDirectory(), second.GetDirectory()) assert.Equal(t, firstFiles, dirFiles(t, fs, first.GetDirectory())) } + +// TestGPGDecryptErrorHasGPGReason runs the real gpg on data that is not an +// OpenPGP message and checks that the error carries the reason gpg writes +// to stderr. +func TestGPGDecryptErrorHasGPGReason(t *testing.T) { + // The empty GNUPGHOME keeps gpg away from the user's own keyring; + // LC_ALL=C keeps gpg's message in English. + t.Setenv("GNUPGHOME", t.TempDir()) + t.Setenv("LC_ALL", "C") + + _, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message")) + require.ErrorContains(t, err, "gpg: no valid OpenPGP data found") +}