check / check (push) Failing after 2s
vault.CreateVault takes the unlocker passphrase and writes the vault directory, its metadata, long-term public key and passphrase unlocker into a temporary directory, renames that into vaults.d once complete, and only then makes the vault current. secret init and secret vault create call it once instead of adding the unlocker afterwards, so a kill part-way leaves either no vault, whose temporary directory the next command that takes the lock deletes, or a complete one. A test records the state directory before every change the call makes and checks each state, and the command run again from it. Model: opus-5-5
639 lines
17 KiB
Go
639 lines
17 KiB
Go
//go:build darwin
|
|
|
|
package secret_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// pgpUnlockerType is the type of a PGP unlocker.
|
|
const pgpUnlockerType = "pgp"
|
|
|
|
var errNilDataBuffer = errors.New("data buffer is nil")
|
|
|
|
// setupNonInteractiveGPG creates a custom GPG environment for testing
|
|
func setupNonInteractiveGPG(t *testing.T, _, passphrase, gnupgHomeDir string) {
|
|
t.Helper()
|
|
|
|
// Create GPG config file for non-interactive operation
|
|
gpgConfPath := filepath.Join(gnupgHomeDir, "gpg.conf")
|
|
gpgConfContent := `batch
|
|
no-tty
|
|
pinentry-mode loopback
|
|
`
|
|
|
|
err := os.WriteFile(gpgConfPath, []byte(gpgConfContent), 0o600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write GPG config file: %v", err)
|
|
}
|
|
|
|
// Create a test-specific GPG implementation
|
|
origEncryptFunc := secret.GPGEncryptFunc
|
|
origDecryptFunc := secret.GPGDecryptFunc
|
|
|
|
// Set custom GPG functions for this test
|
|
secret.GPGEncryptFunc = func(
|
|
data *memguard.LockedBuffer, keyID string,
|
|
) ([]byte, error) {
|
|
if data == nil {
|
|
return nil, errNilDataBuffer
|
|
}
|
|
|
|
//nolint:gosec // G204: test runs gpg with test-controlled arguments
|
|
cmd := exec.CommandContext(t.Context(), "gpg",
|
|
"--homedir", gnupgHomeDir,
|
|
"--batch",
|
|
"--yes",
|
|
"--pinentry-mode", "loopback",
|
|
"--passphrase", passphrase,
|
|
"--trust-model", "always",
|
|
"--armor",
|
|
"--encrypt",
|
|
"-r", keyID)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Stdin = bytes.NewReader(data.Bytes())
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GPG encryption failed: %w\nStderr: %s", err, stderr.String())
|
|
}
|
|
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
secret.GPGDecryptFunc = func(encryptedData []byte) (*memguard.LockedBuffer, error) {
|
|
//nolint:gosec // G204: test runs gpg with test-controlled arguments
|
|
cmd := exec.CommandContext(t.Context(), "gpg",
|
|
"--homedir", gnupgHomeDir,
|
|
"--batch",
|
|
"--yes",
|
|
"--pinentry-mode", "loopback",
|
|
"--passphrase", passphrase,
|
|
"--quiet",
|
|
"--decrypt")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Stdin = bytes.NewReader(encryptedData)
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GPG decryption failed: %w\nStderr: %s", err, stderr.String())
|
|
}
|
|
|
|
// Create a secure buffer for the decrypted data
|
|
return memguard.NewBufferFromBytes(stdout.Bytes()), nil
|
|
}
|
|
|
|
// Restore original functions after test
|
|
t.Cleanup(func() {
|
|
secret.GPGEncryptFunc = origEncryptFunc
|
|
secret.GPGDecryptFunc = origDecryptFunc
|
|
})
|
|
}
|
|
|
|
// runGPGWithPassphrase executes a GPG command with the specified passphrase
|
|
func runGPGWithPassphrase(
|
|
ctx context.Context,
|
|
gnupgHome, passphrase string, args []string, input io.Reader,
|
|
) ([]byte, error) {
|
|
cmdArgs := append([]string{
|
|
"--homedir=" + gnupgHome,
|
|
"--batch",
|
|
"--yes",
|
|
"--pinentry-mode", "loopback",
|
|
"--passphrase", passphrase,
|
|
}, args...)
|
|
|
|
//nolint:gosec // G204: test runs gpg with test-controlled arguments
|
|
cmd := exec.CommandContext(ctx, "gpg", cmdArgs...)
|
|
cmd.Stdin = input
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GPG command failed: %w\nStderr: %s", err, stderr.String())
|
|
}
|
|
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
// generateTestGPGKey generates a GPG key protected by passphrase in
|
|
// gnupgHomeDir and returns its key ID and fingerprint.
|
|
func generateTestGPGKey(
|
|
t *testing.T, tempDir, gnupgHomeDir, passphrase string,
|
|
) (string, string) {
|
|
t.Helper()
|
|
|
|
// Create GPG batch file for key generation
|
|
batchFile := filepath.Join(tempDir, "gen-key-batch")
|
|
batchContent := `%echo Generating a test key
|
|
Key-Type: RSA
|
|
Key-Length: 2048
|
|
Name-Real: Test User
|
|
Name-Email: test@example.com
|
|
Expire-Date: 0
|
|
Passphrase: ` + passphrase + `
|
|
%commit
|
|
%echo Key generation completed
|
|
`
|
|
|
|
err := os.WriteFile(batchFile, []byte(batchContent), 0o600)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write batch file: %v", err)
|
|
}
|
|
|
|
// Generate GPG key with batch mode
|
|
t.Log("Generating GPG key...")
|
|
|
|
_, err = runGPGWithPassphrase(t.Context(), gnupgHomeDir, passphrase,
|
|
[]string{"--gen-key", batchFile}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate GPG key: %v", err)
|
|
}
|
|
|
|
t.Log("GPG key generated successfully")
|
|
|
|
// Get the key ID and fingerprint
|
|
output, err := runGPGWithPassphrase(t.Context(), gnupgHomeDir, passphrase,
|
|
[]string{"--list-secret-keys", "--with-colons", "--fingerprint"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to list GPG keys: %v", err)
|
|
}
|
|
|
|
// Parse output to get key ID and fingerprint
|
|
var keyID, fingerprint string
|
|
|
|
for line := range strings.SplitSeq(string(output), "\n") {
|
|
if strings.HasPrefix(line, "sec:") {
|
|
fields := strings.Split(line, ":")
|
|
if len(fields) >= 5 {
|
|
keyID = fields[4]
|
|
}
|
|
} else if strings.HasPrefix(line, "fpr:") {
|
|
fields := strings.Split(line, ":")
|
|
if len(fields) >= 10 && fields[9] != "" {
|
|
fingerprint = fields[9]
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if keyID == "" {
|
|
t.Fatalf("Failed to find GPG key ID in output: %s", output)
|
|
}
|
|
|
|
if fingerprint == "" {
|
|
t.Fatalf("Failed to find GPG fingerprint in output: %s", output)
|
|
}
|
|
|
|
t.Logf("Generated GPG key ID: %s", keyID)
|
|
t.Logf("Generated GPG fingerprint: %s", fingerprint)
|
|
|
|
return keyID, fingerprint
|
|
}
|
|
|
|
//nolint:paralleltest // t.Setenv forbids parallel subtests
|
|
func TestPGPUnlockerWithRealFS(t *testing.T) {
|
|
// Check if gpg is available
|
|
_, err := exec.LookPath("gpg")
|
|
if err != nil {
|
|
t.Log("GPG not available, PGP unlock key tests may not fully function")
|
|
// Continue anyway to test what we can
|
|
}
|
|
|
|
// Create a temporary directory for our tests. Not t.TempDir: its longer
|
|
// path would put gpg-agent's socket in GNUPGHOME past the 104-byte limit
|
|
// macOS sets on socket paths.
|
|
//
|
|
//nolint:usetesting // see the comment above
|
|
tempDir, err := os.MkdirTemp("", "secret-pgp-test-")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDir) }() // Clean up after test
|
|
|
|
// Create a temporary GNUPGHOME
|
|
gnupgHomeDir := filepath.Join(tempDir, "gnupg")
|
|
|
|
err = os.MkdirAll(gnupgHomeDir, 0o700)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create GNUPGHOME: %v", err)
|
|
}
|
|
|
|
// Set new GNUPGHOME
|
|
t.Setenv("GNUPGHOME", gnupgHomeDir)
|
|
|
|
// Test passphrase for GPG key
|
|
testPassphrase := "test123"
|
|
|
|
// Setup non-interactive GPG with custom functions
|
|
setupNonInteractiveGPG(t, tempDir, testPassphrase, gnupgHomeDir)
|
|
|
|
keyID, fingerprint := generateTestGPGKey(t, tempDir, gnupgHomeDir, testPassphrase)
|
|
|
|
// Set the GPG_AGENT_INFO to empty to ensure gpg-agent doesn't interfere
|
|
t.Setenv("GPG_AGENT_INFO", "")
|
|
|
|
// Use the real filesystem
|
|
fs := afero.NewOsFs()
|
|
|
|
// Set test environment variables
|
|
t.Setenv(secret.EnvGPGKeyID, keyID)
|
|
|
|
// Set up vault structure for testing
|
|
stateDir := tempDir
|
|
vaultName := "test-vault"
|
|
|
|
// Test creation of a PGP unlock key through a vault
|
|
t.Run("CreatePGPUnlocker", func(t *testing.T) {
|
|
testCreatePGPUnlocker(t, fs, stateDir, vaultName, keyID, fingerprint)
|
|
})
|
|
|
|
// Set up key directory for individual tests
|
|
unlockerDir := filepath.Join(tempDir, "unlocker")
|
|
|
|
err = os.MkdirAll(unlockerDir, secret.DirPerms)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create unlocker directory: %v", err)
|
|
}
|
|
|
|
// Set up test metadata
|
|
metadata := secret.UnlockerMetadata{
|
|
Type: pgpUnlockerType,
|
|
CreatedAt: time.Now(),
|
|
Flags: []string{"gpg", "encrypted"},
|
|
}
|
|
|
|
// Create a PGP unlocker for the remaining tests
|
|
unlocker := secret.NewPGPUnlocker(fs, unlockerDir, metadata)
|
|
|
|
// Test getting GPG key ID
|
|
t.Run("GetGPGKeyID", func(t *testing.T) {
|
|
testGetGPGKeyID(t, fs, unlocker, unlockerDir, metadata, fingerprint)
|
|
})
|
|
|
|
// Test getting identity from PGP unlocker
|
|
t.Run("GetIdentity", func(t *testing.T) {
|
|
testPGPUnlockerGetIdentity(t, fs, unlocker, unlockerDir, keyID)
|
|
})
|
|
|
|
// Test removing the unlocker
|
|
t.Run("RemoveUnlocker", func(t *testing.T) {
|
|
testRemovePGPUnlocker(t, fs, unlocker, unlockerDir)
|
|
})
|
|
}
|
|
|
|
// testCreatePGPUnlocker creates a vault with a passphrase unlocker, then a
|
|
// PGP unlocker for the GPG key keyID, and checks the PGP unlocker's files
|
|
// and metadata.
|
|
func testCreatePGPUnlocker(
|
|
t *testing.T, fs afero.Fs, stateDir, vaultName, keyID, fingerprint string,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Set a limited test timeout to avoid hanging
|
|
timer := time.AfterFunc(30*time.Second, func() {
|
|
t.Fatalf("Test timed out after 30 seconds")
|
|
})
|
|
defer timer.Stop()
|
|
|
|
mnemonic := testMnemonicBuffer(t)
|
|
|
|
// Create a test vault directory structure
|
|
vlt, err := vault.CreateVault(fs, stateDir, vaultName, mnemonic, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vault: %v", err)
|
|
}
|
|
|
|
// Set the current vault
|
|
err = vault.SelectVault(fs, stateDir, vaultName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to select vault: %v", err)
|
|
}
|
|
|
|
// Derive long-term key from mnemonic
|
|
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
|
|
if err != nil {
|
|
t.Fatalf("Failed to derive long-term key: %v", err)
|
|
}
|
|
|
|
// Get the vault directory
|
|
vaultDir, err := vlt.GetDirectory()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get vault directory: %v", err)
|
|
}
|
|
|
|
// Write long-term public key
|
|
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
|
|
|
|
err = afero.WriteFile(fs, ltPubKeyPath,
|
|
[]byte(ltIdentity.Recipient().String()), secret.FilePerms)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write long-term public key: %v", err)
|
|
}
|
|
|
|
// Unlock the vault
|
|
vlt.Unlock(ltIdentity)
|
|
|
|
// Create a passphrase unlocker first (to have current unlocker)
|
|
passphraseBuffer := memguard.NewBufferFromBytes([]byte("test-passphrase"))
|
|
defer passphraseBuffer.Destroy()
|
|
|
|
passUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create passphrase unlocker: %v", err)
|
|
}
|
|
|
|
// Verify passphrase unlocker was created
|
|
if passUnlocker == nil {
|
|
t.Fatal("Passphrase unlocker is nil")
|
|
}
|
|
|
|
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
|
pgpUnlocker, err := secret.CreatePGPUnlocker(
|
|
fs, stateDir, keyID, fingerprint, mnemonic, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
|
}
|
|
|
|
// Verify the PGP unlock key was created
|
|
if pgpUnlocker == nil {
|
|
t.Fatal("PGP unlock key is nil")
|
|
}
|
|
|
|
// Check if the key has the correct type
|
|
if pgpUnlocker.GetType() != pgpUnlockerType {
|
|
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpUnlocker.GetType())
|
|
}
|
|
|
|
// Check if the key ID includes the GPG fingerprint
|
|
if !strings.Contains(pgpUnlocker.GetID(), fingerprint) {
|
|
t.Errorf("PGP unlock key ID '%s' does not contain GPG fingerprint '%s'",
|
|
pgpUnlocker.GetID(), fingerprint)
|
|
}
|
|
|
|
checkPGPUnlockerFiles(t, fs, pgpUnlocker.GetDirectory())
|
|
checkPGPUnlockerMetadata(t, fs, pgpUnlocker.GetDirectory(), fingerprint)
|
|
}
|
|
|
|
// checkPGPUnlockerFiles checks that the PGP unlocker in unlockerDir has all
|
|
// its files.
|
|
func checkPGPUnlockerFiles(t *testing.T, fs afero.Fs, unlockerDir string) {
|
|
t.Helper()
|
|
|
|
// Check if the key directory exists
|
|
keyExists, err := afero.DirExists(fs, unlockerDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if PGP key directory exists: %v", err)
|
|
}
|
|
|
|
if !keyExists {
|
|
t.Errorf("PGP unlock key directory does not exist: %s", unlockerDir)
|
|
}
|
|
|
|
// Check if required files exist
|
|
recipientPath := filepath.Join(unlockerDir, "pub.txt")
|
|
|
|
recipientExists, err := afero.Exists(fs, recipientPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if recipient file exists: %v", err)
|
|
}
|
|
|
|
if !recipientExists {
|
|
t.Errorf("PGP unlock key recipient file does not exist: %s", recipientPath)
|
|
}
|
|
|
|
privKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
|
|
|
privKeyExists, err := afero.Exists(fs, privKeyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if private key file exists: %v", err)
|
|
}
|
|
|
|
if !privKeyExists {
|
|
t.Errorf("PGP unlock key private key file does not exist: %s", privKeyPath)
|
|
}
|
|
|
|
metadataPath := filepath.Join(unlockerDir, unlockerMetadataFile)
|
|
|
|
metadataExists, err := afero.Exists(fs, metadataPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if metadata file exists: %v", err)
|
|
}
|
|
|
|
if !metadataExists {
|
|
t.Errorf("PGP unlock key metadata file does not exist: %s", metadataPath)
|
|
}
|
|
|
|
longtermPath := filepath.Join(unlockerDir, "longterm.age")
|
|
|
|
longtermExists, err := afero.Exists(fs, longtermPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if longterm key file exists: %v", err)
|
|
}
|
|
|
|
if !longtermExists {
|
|
t.Errorf("PGP unlock key longterm key file does not exist: %s", longtermPath)
|
|
}
|
|
}
|
|
|
|
// checkPGPUnlockerMetadata checks that the metadata of the PGP unlocker in
|
|
// unlockerDir names its type and the GPG key by fingerprint.
|
|
func checkPGPUnlockerMetadata(
|
|
t *testing.T, fs afero.Fs, unlockerDir, fingerprint string,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Read and verify metadata
|
|
metadataPath := filepath.Join(unlockerDir, unlockerMetadataFile)
|
|
|
|
metadataBytes, err := afero.ReadFile(fs, metadataPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read metadata: %v", err)
|
|
}
|
|
|
|
var metadata struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
Flags []string `json:"flags"`
|
|
GPGKeyID string `json:"gpgKeyId"`
|
|
}
|
|
|
|
err = json.Unmarshal(metadataBytes, &metadata)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse metadata: %v", err)
|
|
}
|
|
|
|
if metadata.Type != pgpUnlockerType {
|
|
t.Errorf("Expected metadata type 'pgp', got '%s'", metadata.Type)
|
|
}
|
|
|
|
if metadata.GPGKeyID != fingerprint {
|
|
t.Errorf("Expected GPG fingerprint '%s', got '%s'", fingerprint, metadata.GPGKeyID)
|
|
}
|
|
}
|
|
|
|
// testGetGPGKeyID writes PGP unlocker metadata holding the GPG fingerprint
|
|
// into unlockerDir and checks that unlocker reads it back.
|
|
func testGetGPGKeyID(
|
|
t *testing.T, fs afero.Fs, unlocker *secret.PGPUnlocker,
|
|
unlockerDir string, metadata secret.UnlockerMetadata, fingerprint string,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Create PGP metadata with GPG key ID
|
|
type PGPUnlockerMetadata struct {
|
|
secret.UnlockerMetadata
|
|
|
|
GPGKeyID string `json:"gpgKeyId"`
|
|
}
|
|
|
|
pgpMetadata := PGPUnlockerMetadata{
|
|
UnlockerMetadata: metadata,
|
|
GPGKeyID: fingerprint,
|
|
}
|
|
|
|
// Write metadata file
|
|
metadataPath := filepath.Join(unlockerDir, unlockerMetadataFile)
|
|
|
|
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal metadata: %v", err)
|
|
}
|
|
|
|
err = afero.WriteFile(fs, metadataPath, metadataBytes, secret.FilePerms)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write metadata: %v", err)
|
|
}
|
|
|
|
// Get GPG key ID
|
|
retrievedKeyID, err := unlocker.GetGPGKeyID()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get GPG key ID: %v", err)
|
|
}
|
|
|
|
// Verify key ID (should be the fingerprint)
|
|
if retrievedKeyID != fingerprint {
|
|
t.Errorf("Expected GPG fingerprint '%s', got '%s'", fingerprint, retrievedKeyID)
|
|
}
|
|
}
|
|
|
|
// testPGPUnlockerGetIdentity writes an age identity encrypted to the GPG key
|
|
// keyID into unlockerDir and checks that unlocker decrypts it.
|
|
func testPGPUnlockerGetIdentity(
|
|
t *testing.T, fs afero.Fs, unlocker *secret.PGPUnlocker,
|
|
unlockerDir, keyID string,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Generate an age identity for testing
|
|
ageIdentity, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate age identity: %v", err)
|
|
}
|
|
|
|
// Write the recipient
|
|
recipientPath := filepath.Join(unlockerDir, "pub.txt")
|
|
|
|
err = afero.WriteFile(fs, recipientPath,
|
|
[]byte(ageIdentity.Recipient().String()), secret.FilePerms)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write recipient: %v", err)
|
|
}
|
|
|
|
// GPG encrypt the private key using our custom encrypt function
|
|
privKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
|
|
defer privKeyBuffer.Destroy()
|
|
|
|
encryptedOutput, err := secret.GPGEncryptFunc(privKeyBuffer, keyID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to encrypt with GPG: %v", err)
|
|
}
|
|
|
|
// Write the encrypted data to a file
|
|
encryptedPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
|
|
|
err = afero.WriteFile(fs, encryptedPath, encryptedOutput, secret.FilePerms)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write encrypted private key: %v", err)
|
|
}
|
|
|
|
// Now try to get the identity - this will use our custom GPGDecryptFunc
|
|
identity, err := unlocker.GetIdentity()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get identity: %v", err)
|
|
}
|
|
|
|
// Verify the identity matches
|
|
expectedPubKey := ageIdentity.Recipient().String()
|
|
actualPubKey := identity.Recipient().String()
|
|
|
|
if actualPubKey != expectedPubKey {
|
|
t.Errorf("Expected public key '%s', got '%s'", expectedPubKey, actualPubKey)
|
|
}
|
|
}
|
|
|
|
// testRemovePGPUnlocker removes unlocker and checks that unlockerDir is gone.
|
|
func testRemovePGPUnlocker(
|
|
t *testing.T, fs afero.Fs, unlocker *secret.PGPUnlocker, unlockerDir string,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Ensure unlocker directory exists before removal
|
|
keyExists, err := afero.DirExists(fs, unlockerDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
|
}
|
|
|
|
if !keyExists {
|
|
t.Fatalf("Unlocker directory does not exist: %s", unlockerDir)
|
|
}
|
|
|
|
// Remove unlocker
|
|
err = unlocker.Remove()
|
|
if err != nil {
|
|
t.Fatalf("Failed to remove unlocker: %v", err)
|
|
}
|
|
|
|
// Verify directory is gone
|
|
keyExists, err = afero.DirExists(fs, unlockerDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
|
}
|
|
|
|
if keyExists {
|
|
t.Errorf("Unlocker directory still exists after removal: %s", unlockerDir)
|
|
}
|
|
}
|