Files
vault/vault_test.go
T
Andy Dixon ec204f0ea2 Add cp, folder mv, get -r, and unlock/lock to remember the key
Filesystem basics: mv now moves folders as well as files, and like mv(1)
a NEW ending in / or naming an existing folder means into it. cp copies a
file, or a folder with -r, by writing new records that share the
original's objects, so it is instant and takes no space; rm already keeps
objects another record still uses. New records go first, so an
interrupted move leaves both names, never neither. get -r extracts a
folder, keeping its structure.

vault unlock [--for 5m] checks the passphrase and starts the binary again
as a hidden __agent process, handing it the suite ID and seed over a pipe.
The agent keeps them in mlocked memory, with core dumps off and (on Linux)
PR_SET_DUMPABLE cleared, serves them on a Unix socket in a 0700 per-user
directory whose owner is checked on every use, and wipes them and exits
at a hard deadline or on vault lock. Commands ask it first unless
VAULT_PASSPHRASE is set, and genkey stops any agent left for that path.

Guide, man pages (new vault-cp, vault-unlock, vault-lock), README and
screenshots updated; the guide's error table is split so it fits.
2026-10-02 22:13:06 +01:00

693 lines
20 KiB
Go

// Copyright 2026 The vault Authors.
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"crypto/rand"
"encoding/binary"
"io"
"io/fs"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
"time"
)
func TestKeyFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "key")
if err := writeKeyFile(path, "pass", ""); err != nil {
t.Fatal(err)
}
if _, err := unlockKeyFile(path, "wrong"); err == nil {
t.Fatal("wrong passphrase unlocked the key")
}
if _, err := unlockKeyFile(path, "pass"); err != nil {
t.Fatal(err)
}
}
func newTestStore(t *testing.T, keyPath, pass string) *store {
t.Helper()
id, err := unlockKeyFile(keyPath, pass)
if err != nil {
t.Fatal(err)
}
s, err := openStore(filepath.Join(filepath.Dir(keyPath), "store"), id)
if err != nil {
t.Fatal(err)
}
return s
}
func TestStoreLifecycle(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
src := filepath.Join(dir, "in")
want := bytes.Repeat([]byte("x"), 70000)
os.WriteFile(src, want, 0o640)
cat, _ := s.catalogue()
if _, err := s.add(cat, src, "a/b"); err != nil {
t.Fatal(err)
}
if replaced, err := s.add(cat, src, "a/b"); err != nil || !replaced {
t.Fatalf("replace: %v %v", replaced, err)
}
if _, err := s.add(cat, src, "c"); err != nil {
t.Fatal(err)
}
if cat, _ = s.catalogue(); len(cat) != 2 || len(cat["a/b"]) != 1 {
t.Fatalf("catalogue = %v", cat)
}
pairs, err := transfers(cat, "a/b", "d", false)
if err != nil {
t.Fatal(err)
}
if err := s.copyAll(cat, pairs, true, func(string, string) {}); err != nil {
t.Fatal(err)
}
out := filepath.Join(dir, "out")
if err := s.get("d", out); err != nil {
t.Fatal(err)
}
if got, _ := os.ReadFile(out); !bytes.Equal(got, want) {
t.Fatal("round trip mismatch")
}
if err := s.get("d", out); err == nil {
t.Fatal("get overwrote an existing file")
}
// Swapping one object for another must be detected.
cat, _ = s.catalogue()
d, c := cat["d"][0], cat["c"][0]
data, _ := os.ReadFile(s.path(objectsDir, objectID(c.Seed, 0)))
os.WriteFile(s.path(objectsDir, objectID(d.Seed, 0)), data, 0o600)
if err := s.get("d", filepath.Join(dir, "swapped")); err == nil {
t.Fatal("swapped object was accepted")
}
if err := s.delete("c"); err != nil {
t.Fatal(err)
}
if cat, _ = s.catalogue(); len(cat) != 1 {
t.Fatalf("after delete: %v", cat)
}
}
func TestDecoy(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "real", "decoy"); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
src := filepath.Join(dir, "in")
os.WriteFile(src, []byte("data"), 0o600)
real, decoy := newTestStore(t, keyPath, "real"), newTestStore(t, keyPath, "decoy")
for s, name := range map[*store]string{real: "secret", decoy: "shopping"} {
cat, err := s.catalogue()
if err != nil || len(cat) != 0 {
t.Fatalf("fresh vault catalogue = %v, %v", cat, err)
}
if _, err := s.add(cat, src, name); err != nil {
t.Fatal(err)
}
}
for s, name := range map[*store]string{real: "secret", decoy: "shopping"} {
if cat, _ := s.catalogue(); len(cat) != 1 || cat[name] == nil {
t.Fatalf("catalogue = %v, want only %s", cat, name)
}
}
if _, err := unlockKeyFile(keyPath, "neither"); err == nil {
t.Fatal("wrong passphrase unlocked the key")
}
if n := len(ciphertexts(t, filepath.Join(dir, "store", recordsDir))); n < fillerMin+2 {
t.Fatalf("only %d records; filler missing", n)
}
}
// ciphertexts maps every file under dir to its size.
func ciphertexts(t *testing.T, dir string) map[string]int64 {
t.Helper()
sizes := map[string]int64{}
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
info, err := d.Info()
sizes[p] = info.Size()
return err
})
if err != nil {
t.Fatal(err)
}
return sizes
}
// TestChunks checks a file bigger than one chunk becomes unlinkable 4 MiB
// objects plus a bucket-sized tail, all in their shards, and that reordering
// the chunks is caught.
func TestChunks(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
objects := filepath.Join(s.root, objectsDir)
before := ciphertexts(t, objects)
src := filepath.Join(dir, "in")
want := make([]byte, 2*chunkData+100)
rand.Read(want)
os.WriteFile(src, want, 0o600)
cat, _ := s.catalogue()
if _, err := s.add(cat, src, "big"); err != nil {
t.Fatal(err)
}
cat, _ = s.catalogue()
r := cat["big"][0]
var sizes []int64
for i := range objectCount(r.Size) {
p := s.path(objectsDir, objectID(r.Seed, i))
if filepath.Base(filepath.Dir(p)) != filepath.Base(p)[:2] {
t.Fatalf("%s is in the wrong shard", p)
}
sizes = append(sizes, ciphertexts(t, p)[p])
}
if len(sizes) != 3 || sizes[0] != sizes[1] || sizes[2] >= sizes[0] {
t.Fatalf("object sizes = %v, want two full chunks and a smaller tail", sizes)
}
if n := len(ciphertexts(t, objects)) - len(before); n != 3 {
t.Fatalf("add wrote %d objects, want 3", n)
}
for p, size := range ciphertexts(t, objects) {
if size > sizes[0] {
t.Fatalf("%s is %d bytes, larger than a chunk", p, size)
}
}
out := filepath.Join(dir, "out")
if err := s.get("big", out); err != nil {
t.Fatal(err)
}
if got, _ := os.ReadFile(out); !bytes.Equal(got, want) {
t.Fatal("round trip mismatch")
}
a, b := s.path(objectsDir, objectID(r.Seed, 0)), s.path(objectsDir, objectID(r.Seed, 1))
os.Rename(a, a+".x")
os.Rename(b, a)
os.Rename(a+".x", b)
if err := s.get("big", filepath.Join(dir, "swapped")); err == nil {
t.Fatal("reordered chunks were accepted")
}
if err := s.delete("big"); err != nil {
t.Fatal(err)
}
if n := len(ciphertexts(t, objects)); n != len(before) {
t.Fatalf("%d objects after rm, want %d", n, len(before))
}
}
// TestLockExcludesOtherProcess runs a copy of the test binary that holds the
// exclusive lock, then checks this process has to wait for it.
func TestLockExcludesOtherProcess(t *testing.T) {
if dir := os.Getenv("VAULT_TEST_LOCK_HOLDER"); dir != "" {
release, err := lockVault(dir, true)
if err != nil {
os.Exit(1)
}
os.Stdout.WriteString("locked\n")
io.Copy(io.Discard, os.Stdin) // hold until the parent closes stdin
release()
os.Exit(0)
}
dir := t.TempDir()
holder := exec.Command(os.Args[0], "-test.run=^TestLockExcludesOtherProcess$")
holder.Env = append(os.Environ(), "VAULT_TEST_LOCK_HOLDER="+dir)
stdin, _ := holder.StdinPipe()
stdout, _ := holder.StdoutPipe()
if err := holder.Start(); err != nil {
t.Fatal(err)
}
defer holder.Wait()
if line, _ := bufio.NewReader(stdout).ReadString('\n'); line != "locked\n" {
t.Fatalf("holder said %q", line)
}
waited := make(chan bool, 1)
go func() {
time.Sleep(200 * time.Millisecond)
stdin.Close() // let the holder go only after we have started waiting
}()
for _, exclusive := range []bool{false, true} {
release, err := acquireLock(filepath.Join(dir, lockName), exclusive, func() { waited <- true })
if err != nil {
t.Fatal(err)
}
release()
if !exclusive && len(waited) == 0 {
t.Fatal("shared lock did not wait for the exclusive holder")
}
}
}
func TestParseSize(t *testing.T) {
for in, want := range map[string]int64{"0": 0, "512": 512, "2k": 2 << 10, "500M": 500 << 20, "2G": 2 << 30, "1TB": 1 << 40} {
if got, err := parseSize(in); err != nil || got != want {
t.Errorf("parseSize(%q) = %d, %v; want %d", in, got, err, want)
}
}
for _, in := range []string{"", "G", "-1", "2X", "1.5G", "2T"} {
if _, err := parseSize(in); err == nil {
t.Errorf("parseSize(%q) accepted", in)
}
}
}
// TestFiller checks a filler target is met with entries shaped like real
// files: fixed-size records and objects no bigger than a chunk.
func TestFiller(t *testing.T) {
root := filepath.Join(t.TempDir(), "store")
if err := initStore(root, 24<<20); err != nil {
t.Fatal(err)
}
if n := len(ciphertexts(t, filepath.Join(root, recordsDir))); n < fillerMin {
t.Fatalf("%d filler records, want at least %d", n, fillerMin)
}
var total, largest int64
for _, size := range ciphertexts(t, filepath.Join(root, objectsDir)) {
total, largest = total+size, max(largest, size)
}
if total < 24<<20 || largest > chunkSize+chunkSize/64 {
t.Fatalf("filler objects total %d, largest %d", total, largest)
}
}
// TestAddTree migrates "." holding the vault and key: structure is kept,
// originals and emptied directories go, the vault and key are left alone.
// A URL download is then added under a folder.
func TestAddTree(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
os.MkdirAll(filepath.Join(dir, "a", "b"), 0o755)
os.WriteFile(filepath.Join(dir, "x"), []byte("x"), 0o600)
os.WriteFile(filepath.Join(dir, "a", "b", "y"), []byte("y"), 0o600)
t.Chdir(dir)
var skip []os.FileInfo
for _, p := range []string{"store", "key"} {
fi, _ := os.Stat(p)
skip = append(skip, fi)
}
cat, _ := s.catalogue()
if err := addPath(s, cat, "x", "d", false, skip, nil); err != nil {
t.Fatal(err)
}
// Migrating over d/x must ask first, and a no must leave everything.
var asked []string
no := func(names []string) bool { asked = names; return false }
if err := addPath(s, cat, ".", "d", true, skip, no); err == nil || !slices.Equal(asked, []string{"d/x"}) {
t.Fatalf("migrate over d/x: err %v, asked %v", err, asked)
}
if _, err := os.Stat("x"); err != nil || len(cat) != 1 {
t.Fatalf("declined migrate changed things: %v, %d stored", err, len(cat))
}
if err := addPath(s, cat, ".", "d", true, skip, func([]string) bool { return true }); err != nil {
t.Fatal(err)
}
var got []string
for name := range cat {
got = append(got, name)
}
slices.Sort(got)
if !slices.Equal(got, []string{"d/a/b/y", "d/x"}) {
t.Fatalf("stored %v", got)
}
var left []string
entries, _ := os.ReadDir(".")
for _, e := range entries {
left = append(left, e.Name())
}
if !slices.Equal(left, []string{"key", "store"}) {
t.Fatalf("left behind %v", left)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.UserAgent() != chromeUA {
http.Error(w, "bad agent", http.StatusForbidden)
return
}
io.WriteString(w, "body")
}))
defer srv.Close()
if err := addURL(s, cat, srv.URL+"/files/page.html", "web"); err != nil {
t.Fatal(err)
}
if err := s.get("web/page.html", "out"); err != nil {
t.Fatal(err)
}
if b, _ := os.ReadFile("out"); string(b) != "body" {
t.Fatalf("downloaded %q", b)
}
}
// TestMirror crawls a two-page site: links in HTML and CSS become relative,
// off-host links stay, and everything is stored under host/.
func TestMirror(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.UserAgent() != chromeUA {
http.Error(w, "bad agent", http.StatusForbidden)
return
}
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/html")
io.WriteString(w, `<a href="`+srv.URL+`/sub/p.html#x">p</a> <a href="https://elsewhere.example/">e</a> <link href='/s.css'>`)
case "/sub/p.html":
w.Header().Set("Content-Type", "text/html")
io.WriteString(w, `<a href="/">home</a> <img src="../img/a.png">`)
case "/s.css":
w.Header().Set("Content-Type", "text/css")
io.WriteString(w, `body{background:url(/img/a.png)}`)
case "/img/a.png":
io.WriteString(w, "png")
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
cat, _ := s.catalogue()
if err := mirror(s, cat, srv.URL, "m"); err != nil {
t.Fatal(err)
}
host := srv.Listener.Addr().String()
for name, want := range map[string]string{
"m/" + host + "/index.html": `<a href="sub/p.html#x">p</a> <a href="https://elsewhere.example/">e</a> <link href='s.css'>`,
"m/" + host + "/sub/p.html": `<a href="../index.html">home</a> <img src="../img/a.png">`,
"m/" + host + "/s.css": `body{background:url(img/a.png)}`,
"m/" + host + "/img/a.png": "png",
} {
out := filepath.Join(dir, "out")
if err := s.get(name, out); err != nil {
t.Fatal(name, err)
}
if b, _ := os.ReadFile(out); string(b) != want {
t.Errorf("%s = %q, want %q", name, b, want)
}
os.Remove(out)
}
}
func TestAbsorb(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
mtime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
for _, h := range []*tar.Header{
{Name: "./d/", Typeflag: tar.TypeDir, Mode: 0o755},
{Name: "./d/a.txt", Size: 5, Mode: 0o600, ModTime: mtime},
{Name: "../escape", Size: 5, Mode: 0o600},
{Name: "./d/link", Typeflag: tar.TypeSymlink, Linkname: "a.txt"},
} {
tw.WriteHeader(h)
if h.Size > 0 {
io.WriteString(tw, "hello")
}
}
tw.Close()
zw.Close()
cat, _ := s.catalogue()
if err := absorb(s, cat, &buf, "in"); err != nil {
t.Fatal(err)
}
if len(cat) != 1 || len(cat["in/d/a.txt"]) != 1 {
t.Fatalf("catalogue = %v, want only in/d/a.txt", cat)
}
if r := cat["in/d/a.txt"][0]; !r.Modified.Equal(mtime) || r.Mode != 0o600 {
t.Errorf("record = %v, %o", r.Modified, r.Mode)
}
out := filepath.Join(dir, "out")
if err := s.get("in/d/a.txt", out); err != nil {
t.Fatal(err)
}
if b, _ := os.ReadFile(out); string(b) != "hello" {
t.Errorf("got %q", b)
}
}
func TestRemoveTree(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
objects := filepath.Join(dir, "store", "objects")
before := len(ciphertexts(t, objects))
cat, _ := s.catalogue()
src := filepath.Join(dir, "in")
big := make([]byte, chunkData+1) // two objects
rand.Read(big)
for name, body := range map[string][]byte{"a/x": big, "a/b/y": []byte("y"), "ab/z": []byte("z"), "b/w": []byte("w")} {
os.WriteFile(src, body, 0o600)
if _, err := s.add(cat, src, name); err != nil {
t.Fatal(err)
}
}
if n := len(ciphertexts(t, objects)) - before; n != 5 {
t.Fatalf("%d objects added, want 5", n)
}
names := under(cat, "a")
if !slices.Equal(names, []string{"a/b/y", "a/x"}) {
t.Fatalf("under a: %v", names)
}
var done []string
if err := s.removeAll(cat, names, func(n string) { done = append(done, n) }); err != nil {
t.Fatal(err)
}
if !slices.Equal(done, names) || len(cat) != 2 || len(cat["ab/z"]) != 1 || len(cat["b/w"]) != 1 {
t.Fatalf("done %v, left %v", done, cat)
}
if n := len(ciphertexts(t, objects)) - before; n != 2 {
t.Fatalf("%d objects left, want 2: the space was not freed", n)
}
fresh, _ := s.catalogue()
if len(fresh) != 2 {
t.Fatalf("on disk: %v", fresh)
}
out := filepath.Join(dir, "out")
if err := s.get("ab/z", out); err != nil {
t.Fatal(err)
}
}
func TestLs(t *testing.T) {
at := func(m int) time.Time { return time.Date(2026, 1, 1, 12, m, 0, 0, time.UTC) }
cat := map[string][]*record{
"notes.md": {{Size: 10, Modified: at(1)}},
"a/x": {{Size: 100, Modified: at(2)}},
"a/b/y": {{Size: 1000, Modified: at(5)}},
"a/b/c/z": {{Size: 1, Modified: at(3)}},
"ab/z": {{Size: 7, Modified: at(4)}},
"photos/2025/img1": {{Size: 2048, Modified: at(6)}},
}
names := func(folder string) []string {
t.Helper()
var b bytes.Buffer
if err := ls(&b, cat, folder); err != nil {
t.Fatal(err)
}
var got []string
for _, line := range strings.Split(strings.TrimRight(b.String(), "\n"), "\n") {
got = append(got, strings.TrimSpace(line[10:]))
}
return got
}
day := func(m int) string { return at(m).Local().Format("2006-01-02 15:04") + " " }
if got, want := names(""), []string{day(5) + "a/", day(4) + "ab/", day(6) + "photos/", day(1) + "notes.md"}; !slices.Equal(got, want) {
t.Fatalf("ls: %q, want %q", got, want)
}
if got, want := names("a"), []string{day(5) + "b/", day(2) + "x"}; !slices.Equal(got, want) {
t.Fatalf("ls a: %q, want %q", got, want)
}
if got, want := names("a/b"), []string{day(3) + "c/", day(5) + "y"}; !slices.Equal(got, want) {
t.Fatalf("ls a/b: %q, want %q", got, want)
}
if got, want := names("a/x"), []string{day(2) + "x"}; !slices.Equal(got, want) {
t.Fatalf("ls a/x: %q, want %q", got, want)
}
var b bytes.Buffer
ls(&b, cat, "")
if !strings.HasPrefix(b.String(), " 1.1 KiB ") {
t.Fatalf("folder a/ should total 1101 bytes: %q", b.String())
}
if err := ls(&b, cat, "nope"); err == nil {
t.Fatal("ls of a missing folder succeeded")
}
}
func TestCopyMove(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
t.Fatal(err)
}
s := newTestStore(t, keyPath, "pass")
cat, _ := s.catalogue()
src := filepath.Join(dir, "in")
for _, name := range []string{"a/x", "a/b/y", "ab/z", "c/w"} {
os.WriteFile(src, []byte(name), 0o600)
if _, err := s.add(cat, src, name); err != nil {
t.Fatal(err)
}
}
run := func(src, dst string, recursive, move bool) error {
pairs, err := transfers(cat, src, dst, recursive)
if err != nil {
return err
}
return s.copyAll(cat, pairs, move, func(string, string) {})
}
for _, bad := range [][2]string{{"a", "d"}, {"a/x", "ab/z"}, {"nope", "d"}, {"", "d"}} {
if run(bad[0], bad[1], false, false) == nil {
t.Errorf("cp %s %s succeeded", bad[0], bad[1])
}
}
if run("a", "a/b", true, true) == nil {
t.Error("moved a folder into itself")
}
if err := run("a", "d", true, false); err != nil { // cp -r a d
t.Fatal(err)
}
if err := run("d", "c/", true, true); err != nil { // mv d c/
t.Fatal(err)
}
if err := run("ab/z", "c", false, true); err != nil { // mv ab/z into existing folder c
t.Fatal(err)
}
if err := s.removeAll(cat, under(cat, "a"), func(string) {}); err != nil {
t.Fatal(err)
}
fresh, _ := s.catalogue()
var names []string
for n := range fresh {
names = append(names, n)
}
slices.Sort(names)
if want := []string{"c/d/b/y", "c/d/x", "c/w", "c/z"}; !slices.Equal(names, want) {
t.Fatalf("names %v, want %v", names, want)
}
out := filepath.Join(dir, "out")
if err := s.get("c/d/x", out); err != nil {
t.Fatal("copy lost its data when the original went: ", err)
}
if b, _ := os.ReadFile(out); string(b) != "a/x" {
t.Errorf("got %q", b)
}
}
func TestAgent(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", dir)
t.Setenv(passphraseEnv, "") // restored after the test
os.Unsetenv(passphraseEnv) // so keyIdentity asks the agent
keyPath := filepath.Join(dir, "key")
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
t.Fatal(err)
}
id, err := unlockKeyFile(keyPath, "pass")
if err != nil {
t.Fatal(err)
}
if _, err := agentIdentity(keyPath); err == nil {
t.Fatal("identity from an agent that is not running")
}
sock, err := agentSocket(keyPath)
if err != nil {
t.Fatal(err)
}
secret := binary.BigEndian.AppendUint16(nil, id.SuiteID())
secret = append(secret, id.Seed()...)
pr, pw := io.Pipe()
stopped := make(chan struct{})
go func() { runAgent(sock, "1m", bytes.NewReader(secret), pw); close(stopped) }()
if msg, _ := io.ReadAll(pr); string(msg) != "ok" {
t.Fatalf("agent said %q", msg)
}
got, err := keyIdentity(keyPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got.Seed(), id.Seed()) || got.SuiteID() != id.SuiteID() {
t.Fatal("agent returned a different key")
}
if !agentStop(keyPath) {
t.Fatal("agent was not running")
}
<-stopped
if _, err := os.Stat(sock); !os.IsNotExist(err) {
t.Fatal("socket left behind")
}
if agentStop(keyPath) {
t.Fatal("agent still running")
}
}