Make secret unlocker add pgp work on Linux (closes #88)
check / check (push) Failing after 1s

CreatePGPUnlocker got the vault's long-term key from the keychain
unlocker's helper, which on every platform but macOS is a stub that
always fails. It now calls the vault's GetOrDeriveLongTermKey, as adding
a passphrase unlocker does: from the mnemonic, checked against the
vault, or else from the current unlocker. That method joins
VaultInterface. The test GPG key gains an encryption subkey, and a new
test adds a PGP unlocker with the long-term key from the mnemonic and
from a passphrase unlocker, then reads a secret through it.

Model: opus-5-5
This commit was merged in pull request #95.
This commit is contained in:
2026-10-04 14:42:01 +02:00
parent 007254a1f0
commit 62967f28d0
10 changed files with 110 additions and 20 deletions
+2
View File
@@ -8,6 +8,7 @@ import (
"testing"
"time"
"filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
@@ -32,6 +33,7 @@ func (v *realVault) GetFilesystem() afero.Fs { return v.fs }
// Unused by getLongTermPrivateKey — these satisfy VaultInterface.
func (v *realVault) AddSecret(string, *memguard.LockedBuffer, bool) error { panic("not used") }
func (v *realVault) GetCurrentUnlocker() (Unlocker, error) { panic("not used") }
func (v *realVault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) { panic("not used") }
func (v *realVault) CreatePassphraseUnlocker(*memguard.LockedBuffer) (*PassphraseUnlocker, error) {
panic("not used")
}
-8
View File
@@ -6,7 +6,6 @@ import (
"errors"
"filippo.io/age"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
)
@@ -79,10 +78,3 @@ func (k *KeychainUnlocker) Remove() error {
func CreateKeychainUnlocker(_ afero.Fs, _ string) (*KeychainUnlocker, error) {
return nil, errKeychainNotSupported
}
// getLongTermPrivateKey returns an error on non-Darwin platforms
func getLongTermPrivateKey(
_ afero.Fs, _ VaultInterface,
) (*memguard.LockedBuffer, error) {
return nil, errKeychainNotSupported
}
+7 -6
View File
@@ -277,7 +277,7 @@ func CreatePGPUnlocker(
// Step 2: Encrypt the long-term private key to the new keypair, and the
// keypair's private key to the GPG key
encryptedLtPrivKey, encryptedAgePrivKey, err := encryptPGPUnlockerKeys(
fs, vault, ageIdentity, gpgKeyID)
vault, ageIdentity, gpgKeyID)
if err != nil {
return nil, err
}
@@ -316,14 +316,15 @@ func CreatePGPUnlocker(
// to the new PGP unlocker's age keypair, and that keypair's private key
// encrypted to the GPG key gpgKeyID.
func encryptPGPUnlockerKeys(
fs afero.Fs, vault VaultInterface,
ageIdentity *age.X25519Identity, gpgKeyID string,
vault VaultInterface, ageIdentity *age.X25519Identity, gpgKeyID string,
) ([]byte, []byte, error) {
// Get or derive the long-term private key
ltPrivKeyData, err := getLongTermPrivateKey(fs, vault)
// From the mnemonic or the current unlocker, as for a passphrase unlocker
ltIdentity, err := vault.GetOrDeriveLongTermKey()
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("failed to get long-term key: %w", err)
}
ltPrivKeyData := memguard.NewBufferFromBytes([]byte(ltIdentity.String()))
defer ltPrivKeyData.Destroy()
encryptedLtPrivKey, err := EncryptToRecipient(
+1 -3
View File
@@ -40,9 +40,7 @@ func installFakeGPG(t *testing.T) {
// TestCreatePGPUnlockerFailureWritesNothing makes CreatePGPUnlocker fail at
// getting the vault's long-term key, which used to come after part of the
// unlocker was written, and asserts that nothing is written. Getting the key
// fails because on macOS there is no mnemonic and no current unlocker, and
// on every other platform it always fails
// (https://git.eeqj.de/sneak/secret/issues/88).
// fails because there is no mnemonic and no current unlocker.
func TestCreatePGPUnlockerFailureWritesNothing(t *testing.T) {
installFakeGPG(t)
t.Setenv(secret.EnvMnemonic, "")
+1
View File
@@ -35,6 +35,7 @@ type VaultInterface interface {
GetName() string
GetFilesystem() afero.Fs
GetCurrentUnlocker() (Unlocker, error)
GetOrDeriveLongTermKey() (*age.X25519Identity, error)
CreatePassphraseUnlocker(
passphrase *memguard.LockedBuffer) (*PassphraseUnlocker, error)
}
+4
View File
@@ -107,6 +107,10 @@ func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) {
return nil, errNotImplementedInMock
}
func (m *MockVault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
return nil, errNotImplementedInMock
}
func (m *MockVault) CreatePassphraseUnlocker(
_ *memguard.LockedBuffer,
) (*PassphraseUnlocker, error) {
+4
View File
@@ -87,6 +87,10 @@ func (m *MockVersionVault) GetCurrentUnlocker() (secret.Unlocker, error) {
return nil, errNotImplementedInMock
}
func (m *MockVersionVault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
return nil, errNotImplementedInMock
}
func (m *MockVersionVault) CreatePassphraseUnlocker(
_ *memguard.LockedBuffer,
) (*secret.PassphraseUnlocker, error) {