mirror of
https://github.com/gopasspw/gopass.git
synced 2026-10-14 23:49:24 +02:00
* fix(age): detach agent subprocess from parent file descriptors When the age agent is started via startAgent(), it inherits the parent process's stderr (explicitly via cmd.Stderr = os.Stderr) and stdin/stdout (implicitly by not setting them). This causes the parent process to hang when it captures its own output via exec.Cmd.CombinedOutput() or similar pipe-based methods, because the agent subprocess keeps the pipe file descriptors open indefinitely as a daemon. This affects: - Integration tests that use exec.Command + CombinedOutput() - Any external tool that wraps gopass as a subprocess and captures its output The fix sets cmd.Stdin, cmd.Stdout, and cmd.Stderr to nil for the agent process, ensuring it is fully detached from the parent's file descriptors. The agent already uses a Unix domain socket for communication, so it does not need inherited pipes. The cmd.Env = os.Environ() is preserved so the agent inherits the necessary environment variables (GOPASS_HOMEDIR, GOPASS_AGE_PASSWORD, etc.). * Add GOPASS_AGE_STDIN_PASSPHRASE env variable to bypass pinentry for age * fix(age): reload identities on unlock command (#3363) Fix the 'gopass age agent unlock' command to reload identities after unlocking, instead of just setting locked=false. Before this fix, unlock would only set locked=false but leave identities as nil, causing decrypt operations to fail with 'no identity matched any recipient' error instead of triggering auto-reload. After this fix: - unlock command reloads identities via getAllIds (prompts for PIN) - identities are sent to the agent via SendIdentities - agent timeout is set if configured - message updated to 'Age agent unlocked and identities reloaded' Also adds test infrastructure for age backend testing with embedded test identity. This commit was developed with OpenCode (GLM-5 and Kimi K2.5). Fixes #3363 Signed-off-by: Stéphane Klein <contact@stephane-klein.info> Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * docs(age): document stdin passphrase override Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> --------- Signed-off-by: Stéphane Klein <contact@stephane-klein.info> Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> Co-authored-by: Stéphane Klein <contact@stephane-klein.info>
82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package tests
|
|
|
|
import (
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/gopasspw/gopass/tests/agecan"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAgeAgent(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipping test on windows for now")
|
|
}
|
|
|
|
ts := newTester(t)
|
|
defer ts.teardown()
|
|
|
|
out, err := ts.runCmd([]string{ts.Binary, "age", "identities", "keygen", "--password", "foo"}, []byte("test\ntest\n"))
|
|
require.NoError(t, err, out)
|
|
}
|
|
|
|
func TestAgeAgentUnlockAndDecrypt(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipping test on windows for now")
|
|
}
|
|
|
|
ts := newAgeTester(t)
|
|
defer ts.teardown()
|
|
|
|
// Initialize with passphrase-protected identity.
|
|
// GOPASS_AGE_PASSWORD and GOPASS_AGE_STDIN_PASSPHRASE are already set by newAgeTester.
|
|
// initAgeStore also starts the age agent via tryStartAgent.
|
|
ts.initAgeStore(true)
|
|
|
|
// 1. Insert a secret (agent already has identities in memory from init).
|
|
out, err := ts.runCmd([]string{ts.Binary, "insert", "-m", "test/secret"}, []byte("my secret password\n"))
|
|
require.NoError(t, err, out)
|
|
|
|
// 2. Verify baseline works without passphrase (agent is unlocked).
|
|
require.NoError(t, os.Unsetenv("GOPASS_AGE_PASSWORD"))
|
|
out, err = ts.runCmd([]string{ts.Binary, "show", "test/secret"}, nil)
|
|
require.NoError(t, err, out)
|
|
require.Contains(t, out, "my secret password")
|
|
|
|
// 3. Lock the agent.
|
|
out, err = ts.runCmd([]string{ts.Binary, "age", "agent", "lock"}, nil)
|
|
require.NoError(t, err, out)
|
|
|
|
// 4. Verify agent is locked.
|
|
out, err = ts.runCmd([]string{ts.Binary, "age", "agent", "status"}, nil)
|
|
require.NoError(t, err, out)
|
|
require.Contains(t, out, "locked")
|
|
|
|
// 4a. Verify gopass show works with passphrase via stdin when agent is locked.
|
|
// GOPASS_AGE_PASSWORD is unset (from step 2), so the fallback path
|
|
// uses GOPASS_AGE_STDIN_PASSPHRASE to read from stdin.
|
|
t.Setenv("GOPASS_AGE_STDIN_PASSPHRASE", "1")
|
|
out, err = ts.runCmd([]string{ts.Binary, "show", "test/secret"}, []byte(agecan.TestPin+"\n"))
|
|
require.NoError(t, err, out)
|
|
require.Contains(t, out, "my secret password")
|
|
require.NoError(t, os.Unsetenv("GOPASS_AGE_STDIN_PASSPHRASE"))
|
|
|
|
// 5. Unlock with passphrase via stdin (GOPASS_AGE_STDIN_PASSPHRASE forces CLI fallback).
|
|
t.Setenv("GOPASS_AGE_STDIN_PASSPHRASE", "1")
|
|
out, err = ts.runCmd([]string{ts.Binary, "age", "agent", "unlock"}, []byte(agecan.TestPin+"\n"))
|
|
require.NoError(t, err, out)
|
|
require.Contains(t, out, "Age agent unlocked and identities reloaded")
|
|
require.NoError(t, os.Unsetenv("GOPASS_AGE_STDIN_PASSPHRASE"))
|
|
|
|
// 6. Verify show works without re-prompting for PIN.
|
|
require.NoError(t, os.Unsetenv("GOPASS_AGE_PASSWORD"))
|
|
out, err = ts.runCmd([]string{ts.Binary, "show", "test/secret"}, nil)
|
|
require.NoError(t, err, out)
|
|
require.Contains(t, out, "my secret password")
|
|
|
|
// 7. Cleanup.
|
|
out, err = ts.runCmd([]string{ts.Binary, "age", "agent", "stop"}, nil)
|
|
require.NoError(t, err, out)
|
|
}
|