From 4c1507c463f4e0863390150e3628f9dee0a7c6f4 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sat, 3 Oct 2026 09:44:49 +0100 Subject: [PATCH] Add unlock --idle, and packaging/repo.sh for repo.dixon.cx vault unlock --idle counts the duration from the last command that used the key rather than from the unlock: the agent resets its timer each time it hands the key out. The agent loop no longer needs its own goroutine. packaging/repo.sh builds the dixon-vault .deb, .rpm and Arch packages from a release tag (a clean git archive) with nfpm for amd64, arm64, armv7, 386 and riscv64, then publishes them to the apt, dnf and pacman repositories at repo.dixon.cx: apt-ftparchive with a signed InRelease, createrepo_c (in a Fedora container) with signed RPMs and repomd.xml, and repo-add (in an Arch container) with signed packages and database. Indexes cover every package in the repository, under a lock, so other projects publishing with their copy of the script are kept. The package is dixon-vault because vault is HashiCorp Vault in Arch's [extra] and HashiCorp's own repositories; it conflicts with vault. README, guide and CLAUDE.md point at the repository. --- CLAUDE.md | 8 +- README.md | 14 ++- agent.go | 64 ++++++------ docs/guide/vault-guide.tex | 32 +++++- docs/vault-unlock.1 | 10 +- main.go | 18 ++-- packaging/repo.sh | 197 +++++++++++++++++++++++++++++++++++++ vault_test.go | 38 ++++++- 8 files changed, 334 insertions(+), 47 deletions(-) create mode 100755 packaging/repo.sh diff --git a/CLAUDE.md b/CLAUDE.md index 0357291..b73e585 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,8 +15,9 @@ root that builds a single `vault` binary. Module `github.com/andydixon/vault`. folder, asks unless `-f`), `unlock`/`lock`. - `agent*.go` - `vault unlock` re-executes itself as a hidden `__agent` process holding the suite ID and seed (pipe in, mlocked, undumpable) and - serving them on a Unix socket in a 0700 per-user dir until a hard - deadline. `keyIdentity` asks it first unless `$VAULT_PASSPHRASE` is set. + serving them on a Unix socket in a 0700 per-user dir until a deadline + (fixed, or with `--idle` reset on each use). `keyIdentity` asks it first + unless `$VAULT_PASSPHRASE` is set. - `keyfile.go` - key file with two passphrase slots (Argon2id + XChaCha20-Poly1305 sealing a suite ID and 32-byte master seed). The second slot is either a decoy identity or random bytes; slot order is random. @@ -30,6 +31,9 @@ root that builds a single `vault` binary. Module `github.com/andydixon/vault`. readers shared, taken after the passphrase prompt. - `docs/guide` - LaTeX user guide (`build-pdf.sh` via Tectonic; `shots.py` re-shoots screenshots from the real binary). Keep it in step with the CLI. +- `packaging/repo.sh [TAG]` - builds dixon-vault .deb/.rpm/Arch packages + from a tag with nfpm and publishes them, signed, to the apt/dnf/pacman + repo at repo.dixon.cx (same script, other project block, in ../mapsize). - `build.sh` - cross-compiles every `go tool dist list` target except iOS and android/386, amd64, arm (cgo-only). - `internal/pqcrypt` - file encryption (header + STREAM ChaCha20-Poly1305), diff --git a/README.md b/README.md index b4ded5a..3e6f2de 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ whatever the file's size. Object names derive from a secret seed in the record, so nothing on disk links a file's pieces. Records and objects are spread over 256 subfolders (`records/3f/3fa9…`) to keep directories small. +## Install + +On Debian, Ubuntu, Fedora, RHEL, openSUSE and Arch, install the +`dixon-vault` package from the signed repository at repo.dixon.cx; see +https://dixon.cx/repo for the two or three commands. (The package isn't +called `vault` because that name is HashiCorp Vault's.) Every platform also +has a single binary at https://download.dixon.cx/vault/. + +Releases are packaged and published with `packaging/repo.sh [TAG]`. + ## Build ```sh @@ -65,6 +75,7 @@ vault rm old/report.pdf vault rm -r old # a whole folder; lists it and asks first vault rm -rf old # the same, without asking vault unlock # remember the key for 5 minutes (--for 30m) +vault unlock --idle # ... or until 5 minutes go by unused vault lock # forget it now ``` @@ -85,7 +96,8 @@ scripts. `vault unlock` asks once, then starts a helper process, like `ssh-agent`, that holds the unlocked key in memory until a hard deadline (5 minutes by -default) or `vault lock`. Until then commands skip the prompt, unless +default; with `--idle`, 5 minutes since a command last used it) or +`vault lock`. Until then commands skip the prompt, unless `$VAULT_PASSPHRASE` is set, which always wins. The key reaches the helper over a pipe and never touches the disk; the helper's memory is locked against swapping, it cannot be core-dumped (or, on Linux, debugged), and it diff --git a/agent.go b/agent.go index 0f10406..22dd2e2 100644 --- a/agent.go +++ b/agent.go @@ -26,7 +26,8 @@ import ( // seed over a pipe; it never touches the disk. The agent serves them on a // Unix socket in a directory only this user can enter, keeps them in locked, // undumpable memory where the platform allows, and wipes them and exits at -// a fixed deadline, or on `vault lock`. Using the agent never extends it. +// a deadline, or on `vault lock`. The deadline is fixed, or with --idle it +// moves on each time a command uses the key. const ( agentCmd = "__agent" // hidden: the agent process itself @@ -104,8 +105,8 @@ func agentStop(keyPath string) bool { } // agentStart replaces any agent for keyPath with one holding id until ttl -// has passed. -func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration) error { +// has passed, since it started or, with idle, since the key was last used. +func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration, idle bool) error { sock, err := agentSocket(keyPath) if err != nil { return err @@ -115,7 +116,11 @@ func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration) error if err != nil { return err } - cmd := exec.Command(self, agentCmd, sock, ttl.String()) + mode := "fixed" + if idle { + mode = "idle" + } + cmd := exec.Command(self, agentCmd, sock, ttl.String(), mode) stdin, err := cmd.StdinPipe() if err != nil { return err @@ -150,12 +155,13 @@ func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration) error // runAgent is the agent process: it reads the key from in, says "ok" or // why not on out and closes it, then serves the key on sock until ttl has -// passed or it is told to quit. -func runAgent(sock, ttlArg string, in io.Reader, out io.WriteCloser) { +// passed, since it started or with mode "idle" since the key was last +// handed out, or until it is told to quit. +func runAgent(sock, ttlArg, mode string, in io.Reader, out io.WriteCloser) { report := func(msg string) { io.WriteString(out, msg); out.Close() } ttl, err := time.ParseDuration(ttlArg) - if err != nil || ttl <= 0 { - report("bad duration") + if err != nil || ttl <= 0 || mode != "fixed" && mode != "idle" { + report("bad duration or mode") return } secret := make([]byte, agentReply) @@ -175,44 +181,40 @@ func runAgent(sock, ttlArg string, in io.Reader, out io.WriteCloser) { } os.Chmod(sock, 0o600) report("ok") - done := make(chan struct{}) - time.AfterFunc(ttl, func() { l.Close() }) - go func() { - defer close(done) - for { - c, err := l.Accept() - if err != nil { - return - } - if !serveAgent(c, secret) { - l.Close() - c.Close() - return - } - c.Close() + expiry := time.AfterFunc(ttl, func() { l.Close() }) + for { + c, err := l.Accept() + if err != nil { + break } - }() - <-done + op := serveAgent(c, secret) + c.Close() + if op == agentQuit { + break + } + if op == agentGet && mode == "idle" { + expiry.Reset(ttl) + } + } + l.Close() clear(secret) os.Remove(sock) } -// serveAgent answers one request, reporting whether to keep going. -func serveAgent(c net.Conn, secret []byte) bool { +// serveAgent answers one request and returns its operation. +func serveAgent(c net.Conn, secret []byte) byte { c.SetDeadline(time.Now().Add(agentTimeout)) op := make([]byte, 1) if _, err := io.ReadFull(c, op); err != nil { - return true + return 0 } switch op[0] { case agentGet: c.Write(secret) - return true case agentQuit: clear(secret) - return false } - return true + return op[0] } // keyIdentity unlocks the key at keyPath: from the agent if one is running diff --git a/docs/guide/vault-guide.tex b/docs/guide/vault-guide.tex index b1abae2..f406cbd 100644 --- a/docs/guide/vault-guide.tex +++ b/docs/guide/vault-guide.tex @@ -186,6 +186,26 @@ Download the one file for your system, check its SHA-256, rename it to Or build every platform yourself with \cmd{./build.sh}. \end{tldr} +\section{The easy way on Linux: my package repository} + +On Debian, Ubuntu, Fedora, RHEL, openSUSE or Arch, add my signed repository +once and install the \cmd{dixon-vault} package with your usual package +manager. You get the manual pages too, and new releases arrive with your +normal system updates. On Debian or Ubuntu: + +\begin{shell} +$ sudo install -d -m 0755 /etc/apt/keyrings +$ curl -fsSL https://repo.dixon.cx/dixon.gpg | sudo tee /etc/apt/keyrings/dixon.gpg >/dev/null +$ echo "deb [signed-by=/etc/apt/keyrings/dixon.gpg] https://repo.dixon.cx/apt stable main" | + sudo tee /etc/apt/sources.list.d/dixon.list +$ sudo apt update && sudo apt install dixon-vault +\end{shell} + +The commands for dnf, zypper and pacman are at \file{dixon.cx/repo}. The +package is called \cmd{dixon-vault} because plain \cmd{vault} is HashiCorp +Vault's name; the command it installs is still \cmd{vault}. Everyone else, +read on. + \section{Pick your file} vault is a single self-contained program. No installer, no runtime, no @@ -714,13 +734,16 @@ vault. \begin{shell} $ vault unlock # ask once, remember the key for 5 minutes $ vault unlock --for 30m # or for as long as you say +$ vault unlock --idle # 5 minutes since you last used it $ vault lock # forget it right now \end{shell} After \cmd{unlock}, every command skips the passphrase prompt until the time -is up. It's a hard deadline, counted from when you unlocked, not from when you -last used it, so a laptop left open still locks itself. Run \cmd{unlock} -again to start a fresh countdown. +is up. Normally that's a hard deadline, counted from when you unlocked, so a +laptop left open still locks itself; run \cmd{unlock} again for a fresh +countdown. If you'd rather it stayed unlocked while you're busy, \cmd{--idle} +counts from the last command that used the key instead, so it locks only +after that long without use. \shot{unlock}{Unlock once, work without prompts, lock when you're done} @@ -1362,7 +1385,7 @@ passphrase. Nothing in this chapter helps with that. Back up your key. \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault mv OLD NEW}\\[2pt]\small Rename or move a file or folder. Instant.\end{tcolorbox} \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault cp [-r] OLD NEW}\\[2pt]\small Copy a file, or a folder with -r. Instant, no extra space.\end{tcolorbox} \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault rm [-r] [-f] NAME}\\[2pt]\small Delete a file, or a folder with -r. Frees the space. No undo.\end{tcolorbox} -\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault unlock [--for 5m]}\\[2pt]\small Remember the key, so commands stop asking. Hard deadline.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault unlock [--for 5m] [--idle]}\\[2pt]\small Remember the key, so commands stop asking. Fixed deadline, or since last use.\end{tcolorbox} \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault lock}\\[2pt]\small Forget the remembered key now.\end{tcolorbox} \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault help}\\[2pt]\small Show the command summary.\end{tcolorbox} \begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries man vault}\\[2pt]\small A manual page for vault and for every command.\end{tcolorbox} @@ -1380,6 +1403,7 @@ passphrase. Nothing in this chapter helps with that. Back up your key. --yes & \textrm{---} & ask, for \cmd{migrate} \\ -r & \textrm{---} & \cmd{rm}, \cmd{get}, \cmd{cp}: one file; \cmd{ls}: one level \\ --for DURATION & \textrm{---} & \cmd{5m}, for \cmd{unlock} \\ +--idle & \textrm{---} & \cmd{unlock}: count from the unlock, not the last use \\ -f & \textrm{---} & \cmd{rm}: ask before \cmd{-r}, fail if missing \\ \end{vtable} diff --git a/docs/vault-unlock.1 b/docs/vault-unlock.1 index c5d16cb..7d31654 100644 --- a/docs/vault-unlock.1 +++ b/docs/vault-unlock.1 @@ -5,6 +5,7 @@ vault-unlock, vault-lock \- remember the unlocked key for a while, or forget it .B vault unlock .RB [ \-\-for .IR DURATION ] +.RB [ \-\-idle ] .RB [ \-\-key .IR PATH ] .br @@ -17,8 +18,9 @@ asks for the passphrase, checks it, and starts a helper process, much like .BR ssh\-agent (1), that holds the unlocked key in memory until \fIDURATION\fR has passed (default 5m). Until then every command using the same key file skips the -passphrase prompt. The deadline is fixed when you unlock; using the key does -not extend it. Running +passphrase prompt. By default the deadline is fixed when you unlock and +using the key does not extend it; with \fB\-\-idle\fR it is \fIDURATION\fR +after the last command that used the key. Running .B unlock again replaces the helper with a fresh one. .PP @@ -38,6 +40,10 @@ handing the machine to anyone. .TP .BI \-\-for " DURATION" How long to remember the key, such as 90s, 5m or 2h. Default 5m. +.TP +.B \-\-idle +Count \fIDURATION\fR from the last time a command used the key, rather than +from the unlock, so the key stays while you keep working. .PP See also \fB\-\-key\fR in .BR vault (1). diff --git a/main.go b/main.go index b6f1218..08561b9 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,8 @@ const usage = `Usage: vault rm -r FOLDER [-f] delete a folder and everything under it; asks first unless -f (or -rf) is given vault unlock [--for DURATION] remember the passphrase for DURATION - (default 5m), so commands skip asking + [--idle] (default 5m), so commands skip asking; + with --idle, DURATION since last used vault lock forget it now Options (any command): @@ -91,8 +92,8 @@ func main() { os.Exit(2) } cmd, args := os.Args[1], os.Args[2:] - if cmd == agentCmd && len(args) == 2 { - runAgent(args[0], args[1], os.Stdin, os.Stdout) + if cmd == agentCmd && len(args) == 3 { + runAgent(args[0], args[1], args[2], os.Stdin, os.Stdout) return } fset := flag.NewFlagSet("vault "+cmd, flag.ContinueOnError) @@ -103,6 +104,7 @@ func main() { filler := fset.String("filler", "0", "") yes := fset.Bool("yes", false, "") ttl := fset.Duration("for", 5*time.Minute, "") + idle := fset.Bool("idle", false, "") // rm's -r and -f; Go's flag package has no combined short flags, so // -rf and -fr are spelled out. recurse, force := fset.Bool("r", false, ""), fset.Bool("f", false, "") @@ -295,10 +297,14 @@ func main() { check(err) id, err := unlockKeyFile(*keyPath, pass) check(err) - check(agentStart(*keyPath, id, *ttl)) + check(agentStart(*keyPath, id, *ttl, *idle)) id.Close() - fmt.Fprintf(os.Stderr, "unlocked until %s; vault lock forgets the key sooner\n", - time.Now().Add(*ttl).Format("15:04:05")) + if *idle { + fmt.Fprintf(os.Stderr, "unlocked until unused for %s; vault lock forgets the key sooner\n", *ttl) + } else { + fmt.Fprintf(os.Stderr, "unlocked until %s; vault lock forgets the key sooner\n", + time.Now().Add(*ttl).Format("15:04:05")) + } case "lock": want(0, 0) if agentStop(*keyPath) { diff --git a/packaging/repo.sh b/packaging/repo.sh new file mode 100755 index 0000000..f248dec --- /dev/null +++ b/packaging/repo.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# Build Linux packages (.deb, .rpm, Arch .pkg.tar.zst) for a release tag and +# publish them to the signed apt, dnf and pacman repositories served at +# https://repo.dixon.cx. +# +# packaging/repo.sh # the newest tag +# packaging/repo.sh 0.5 # a given tag +# +# Builds from a clean export of the tag, so the working tree doesn't matter. +# Every index is rebuilt over all the packages in the repository, so other +# projects publishing there with their copy of this script are kept. +# +# Needs: go, docker, gpg, apt-ftparchive (apt-utils), flock. +# REPO_DIR where the repository lives (default below) +# REPO_GNUPGHOME GnuPG home holding the signing key (default below) +# ARCHS Go architectures to package (default below) +# +# The same script, with its own project block, is in ../mapsize/packaging. +set -euo pipefail + +# ---- project --------------------------------------------------------------- +NAME=vault # the command +# The package: plain "vault" is HashiCorp Vault in Arch's [extra] and in +# HashiCorp's own apt and dnf repositories, which would win upgrades. Both +# install /usr/bin/vault, so they conflict. +PACKAGE=dixon-vault +CONFLICTS=vault +SUMMARY="Post-quantum encrypted file vault with a decoy passphrase" +HOMEPAGE=https://dixon.cx/vault +LICENSE=GPL-3.0-or-later +MANPAGES=docs/*.1 +DOCS="README.md LICENSE" +# build OUT: build the binary to OUT, in the source tree, with GOOS, GOARCH +# and GOARM set and VERSION the release. +build() { + CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-s -w -buildid=' -o "$1" . +} +# ---------------------------------------------------------------------------- + +REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html} +REPO_GNUPGHOME=${REPO_GNUPGHOME:-$HOME/.config/repo.dixon.cx/gnupg} +ARCHS=${ARCHS:-"amd64 arm64 arm 386 riscv64"} +NFPM="go run github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.1" +SUITE=stable +COMPONENT=main +ARCH_REPO=dixon # the pacman repository name: [dixon] + +say() { echo "repo: $*" >&2; } +die() { say "$*"; exit 1; } + +for tool in go docker gpg apt-ftparchive flock; do + command -v $tool >/dev/null || die "needs $tool" +done +[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist" +export GNUPGHOME=$REPO_GNUPGHOME +KEY=$(gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr/{print $10; exit}') +[[ -n $KEY ]] || die "no signing key in $GNUPGHOME" + +cd "$(git rev-parse --show-toplevel)" +TAG=${1:-$(git describe --tags --abbrev=0)} +git rev-parse -q --verify "refs/tags/$TAG" >/dev/null || die "no tag $TAG" +VERSION=${TAG#v} +export VERSION + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +git archive "$TAG" | tar -x -C "$WORK" --one-top-level=src +gpg --batch --armor --export-secret-keys "$KEY" >"$WORK/signing.asc" + +# ---- build ----------------------------------------------------------------- +say "building $PACKAGE $VERSION for $ARCHS" +mkdir -p "$WORK/out" +for arch in $ARCHS; do + goarm= nfarch=$arch + [[ $arch == arm ]] && goarm=7 nfarch=arm7 + (cd "$WORK/src" && GOOS=linux GOARCH=$arch GOARM=$goarm build "$WORK/bin-$arch/$NAME") + { + cat <<-EOF + name: $PACKAGE + arch: $nfarch + platform: linux + version: $VERSION + release: 1 + maintainer: Andy Dixon + description: $SUMMARY + homepage: $HOMEPAGE + license: $LICENSE + conflicts: [${CONFLICTS:-}] + contents: + - src: $WORK/bin-$arch/$NAME + dst: /usr/bin/$NAME + file_info: {mode: 0755} + EOF + for m in $(cd "$WORK/src" && ls $MANPAGES); do + printf ' - src: %s\n dst: /usr/share/man/man1/%s\n file_info: {mode: 0644}\n' \ + "$WORK/src/$m" "$(basename "$m")" + done + for d in $DOCS; do + printf ' - src: %s\n dst: /usr/share/doc/%s/%s\n file_info: {mode: 0644}\n' \ + "$WORK/src/$d" "$PACKAGE" "$d" + done + printf 'rpm:\n signature:\n key_file: %s\n' "$WORK/signing.asc" + } >"$WORK/nfpm-$arch.yaml" + for fmt in deb rpm archlinux; do + $NFPM pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >/dev/null + done +done + +# ---- publish --------------------------------------------------------------- +# One publisher at a time: the indexes cover every project's packages. +exec 9>"$REPO_DIR/../.publish.lock" +flock 9 +say "publishing to $REPO_DIR" +gpg --batch --armor --export "$KEY" >"$REPO_DIR/dixon.asc" +gpg --batch --export "$KEY" >"$REPO_DIR/dixon.gpg" +sign() { gpg --batch --yes --local-user "$KEY" "$@"; } + +# apt: pool/main///, indexed by apt-ftparchive. +pool=$REPO_DIR/apt/pool/$COMPONENT/${PACKAGE:0:1}/$PACKAGE +mkdir -p "$pool" +cp "$WORK"/out/*.deb "$pool/" +( + cd "$REPO_DIR/apt" + # Built beside the live suite and swapped in, so clients never see half. + dists=dists/.$SUITE.new + rm -rf "$dists" + debarchs=$(find pool -name '*.deb' | sed -E 's/.*_([^_]+)\.deb$/\1/' | sort -u | tr '\n' ' ') + for a in $debarchs; do + d=$dists/$COMPONENT/binary-$a + mkdir -p "$d" + apt-ftparchive --arch "$a" packages pool >"$d/Packages" + gzip -9kn "$d/Packages" + done + apt-ftparchive \ + -o APT::FTPArchive::Release::Origin=repo.dixon.cx \ + -o APT::FTPArchive::Release::Label=repo.dixon.cx \ + -o APT::FTPArchive::Release::Suite=$SUITE \ + -o APT::FTPArchive::Release::Codename=$SUITE \ + -o APT::FTPArchive::Release::Components=$COMPONENT \ + -o "APT::FTPArchive::Release::Architectures=${debarchs% }" \ + release "$dists" >"$WORK/Release" + mv "$WORK/Release" "$dists/Release" + sign --clearsign -o "$dists/InRelease" "$dists/Release" + sign --armor --detach-sign -o "$dists/Release.gpg" "$dists/Release" + rm -rf "dists/.$SUITE.old" + [[ -d dists/$SUITE ]] && mv "dists/$SUITE" "dists/.$SUITE.old" + mv "$dists" "dists/$SUITE" + rm -rf "dists/.$SUITE.old" +) + +# dnf: every architecture in one directory; dnf picks its own. +mkdir -p "$REPO_DIR/rpm" +cp "$WORK"/out/*.rpm "$REPO_DIR/rpm/" +docker image inspect repo-tools:fedora >/dev/null 2>&1 || + docker build -q -t repo-tools:fedora - >/dev/null <<-'EOF' + FROM fedora:42 + RUN dnf -y install createrepo_c && dnf clean all + EOF +docker run --rm --user "$(id -u):$(id -g)" -v "$REPO_DIR/rpm:/repo" repo-tools:fedora \ + createrepo_c --quiet --update --general-compress-type=gz /repo +sign --armor --detach-sign -o "$REPO_DIR/rpm/repodata/repomd.xml.asc" "$REPO_DIR/rpm/repodata/repomd.xml" +cat >"$REPO_DIR/rpm/dixon.repo" <&1 | + { grep -v "newer version\|^$" >&2 || true; } + for db in $ARCH_REPO.db $ARCH_REPO.files; do + sign --detach-sign -o "$dir/$db.tar.gz.sig" "$dir/$db.tar.gz" + ln -sfn "$db.tar.gz.sig" "$dir/$db.sig" + done + rm -f "$dir"/*.old "$dir"/*.old.sig +done + +say "published $PACKAGE $VERSION: $(cd "$WORK/out" && ls | wc -l) packages" diff --git a/vault_test.go b/vault_test.go index 40b3f69..7758b4e 100644 --- a/vault_test.go +++ b/vault_test.go @@ -21,6 +21,8 @@ import ( "strings" "testing" "time" + + "github.com/andydixon/vault/internal/pqcrypt" ) func TestKeyFile(t *testing.T) { @@ -668,7 +670,7 @@ func TestAgent(t *testing.T) { secret = append(secret, id.Seed()...) pr, pw := io.Pipe() stopped := make(chan struct{}) - go func() { runAgent(sock, "1m", bytes.NewReader(secret), pw); close(stopped) }() + go func() { runAgent(sock, "1m", "fixed", bytes.NewReader(secret), pw); close(stopped) }() if msg, _ := io.ReadAll(pr); string(msg) != "ok" { t.Fatalf("agent said %q", msg) } @@ -690,3 +692,37 @@ func TestAgent(t *testing.T) { t.Fatal("agent still running") } } + +func TestAgentIdle(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_RUNTIME_DIR", dir) + keyPath := filepath.Join(dir, "key") + sock, err := agentSocket(keyPath) + if err != nil { + t.Fatal(err) + } + id, err := pqcrypt.GeneratePQIdentity() + 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, "400ms", "idle", bytes.NewReader(secret), pw); close(stopped) }() + if msg, _ := io.ReadAll(pr); string(msg) != "ok" { + t.Fatalf("agent said %q", msg) + } + // Used every 150ms, it outlives its 400ms by well over twice. + for range 7 { + time.Sleep(150 * time.Millisecond) + if _, err := agentIdentity(keyPath); err != nil { + t.Fatal("idle agent expired while in use: ", err) + } + } + select { + case <-stopped: + case <-time.After(2 * time.Second): + t.Fatal("idle agent outlived its idle time") + } +}