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.
This commit is contained in:
@@ -15,8 +15,9 @@ root that builds a single `vault` binary. Module `github.com/andydixon/vault`.
|
|||||||
folder, asks unless `-f`), `unlock`/`lock`.
|
folder, asks unless `-f`), `unlock`/`lock`.
|
||||||
- `agent*.go` - `vault unlock` re-executes itself as a hidden `__agent`
|
- `agent*.go` - `vault unlock` re-executes itself as a hidden `__agent`
|
||||||
process holding the suite ID and seed (pipe in, mlocked, undumpable) and
|
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
|
serving them on a Unix socket in a 0700 per-user dir until a deadline
|
||||||
deadline. `keyIdentity` asks it first unless `$VAULT_PASSPHRASE` is set.
|
(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 +
|
- `keyfile.go` - key file with two passphrase slots (Argon2id +
|
||||||
XChaCha20-Poly1305 sealing a suite ID and 32-byte master seed). The second
|
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.
|
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.
|
readers shared, taken after the passphrase prompt.
|
||||||
- `docs/guide` - LaTeX user guide (`build-pdf.sh` via Tectonic; `shots.py`
|
- `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.
|
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
|
- `build.sh` - cross-compiles every `go tool dist list` target except
|
||||||
iOS and android/386, amd64, arm (cgo-only).
|
iOS and android/386, amd64, arm (cgo-only).
|
||||||
- `internal/pqcrypt` - file encryption (header + STREAM ChaCha20-Poly1305),
|
- `internal/pqcrypt` - file encryption (header + STREAM ChaCha20-Poly1305),
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -65,6 +75,7 @@ vault rm old/report.pdf
|
|||||||
vault rm -r old # a whole folder; lists it and asks first
|
vault rm -r old # a whole folder; lists it and asks first
|
||||||
vault rm -rf old # the same, without asking
|
vault rm -rf old # the same, without asking
|
||||||
vault unlock # remember the key for 5 minutes (--for 30m)
|
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
|
vault lock # forget it now
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +96,8 @@ scripts.
|
|||||||
|
|
||||||
`vault unlock` asks once, then starts a helper process, like `ssh-agent`,
|
`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
|
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
|
`$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
|
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
|
against swapping, it cannot be core-dumped (or, on Linux, debugged), and it
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import (
|
|||||||
// seed over a pipe; it never touches the disk. The agent serves them on a
|
// 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,
|
// 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
|
// 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 (
|
const (
|
||||||
agentCmd = "__agent" // hidden: the agent process itself
|
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
|
// agentStart replaces any agent for keyPath with one holding id until ttl
|
||||||
// has passed.
|
// has passed, since it started or, with idle, since the key was last used.
|
||||||
func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration) error {
|
func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration, idle bool) error {
|
||||||
sock, err := agentSocket(keyPath)
|
sock, err := agentSocket(keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -115,7 +116,11 @@ func agentStart(keyPath string, id *pqcrypt.PQIdentity, ttl time.Duration) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// 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
|
// why not on out and closes it, then serves the key on sock until ttl has
|
||||||
// passed or it is told to quit.
|
// passed, since it started or with mode "idle" since the key was last
|
||||||
func runAgent(sock, ttlArg string, in io.Reader, out io.WriteCloser) {
|
// 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() }
|
report := func(msg string) { io.WriteString(out, msg); out.Close() }
|
||||||
ttl, err := time.ParseDuration(ttlArg)
|
ttl, err := time.ParseDuration(ttlArg)
|
||||||
if err != nil || ttl <= 0 {
|
if err != nil || ttl <= 0 || mode != "fixed" && mode != "idle" {
|
||||||
report("bad duration")
|
report("bad duration or mode")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
secret := make([]byte, agentReply)
|
secret := make([]byte, agentReply)
|
||||||
@@ -175,44 +181,40 @@ func runAgent(sock, ttlArg string, in io.Reader, out io.WriteCloser) {
|
|||||||
}
|
}
|
||||||
os.Chmod(sock, 0o600)
|
os.Chmod(sock, 0o600)
|
||||||
report("ok")
|
report("ok")
|
||||||
done := make(chan struct{})
|
expiry := time.AfterFunc(ttl, func() { l.Close() })
|
||||||
time.AfterFunc(ttl, func() { l.Close() })
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
for {
|
for {
|
||||||
c, err := l.Accept()
|
c, err := l.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
break
|
||||||
|
}
|
||||||
|
op := serveAgent(c, secret)
|
||||||
|
c.Close()
|
||||||
|
if op == agentQuit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if op == agentGet && mode == "idle" {
|
||||||
|
expiry.Reset(ttl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !serveAgent(c, secret) {
|
|
||||||
l.Close()
|
l.Close()
|
||||||
c.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
<-done
|
|
||||||
clear(secret)
|
clear(secret)
|
||||||
os.Remove(sock)
|
os.Remove(sock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveAgent answers one request, reporting whether to keep going.
|
// serveAgent answers one request and returns its operation.
|
||||||
func serveAgent(c net.Conn, secret []byte) bool {
|
func serveAgent(c net.Conn, secret []byte) byte {
|
||||||
c.SetDeadline(time.Now().Add(agentTimeout))
|
c.SetDeadline(time.Now().Add(agentTimeout))
|
||||||
op := make([]byte, 1)
|
op := make([]byte, 1)
|
||||||
if _, err := io.ReadFull(c, op); err != nil {
|
if _, err := io.ReadFull(c, op); err != nil {
|
||||||
return true
|
return 0
|
||||||
}
|
}
|
||||||
switch op[0] {
|
switch op[0] {
|
||||||
case agentGet:
|
case agentGet:
|
||||||
c.Write(secret)
|
c.Write(secret)
|
||||||
return true
|
|
||||||
case agentQuit:
|
case agentQuit:
|
||||||
clear(secret)
|
clear(secret)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
return op[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyIdentity unlocks the key at keyPath: from the agent if one is running
|
// keyIdentity unlocks the key at keyPath: from the agent if one is running
|
||||||
|
|||||||
@@ -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}.
|
Or build every platform yourself with \cmd{./build.sh}.
|
||||||
\end{tldr}
|
\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}
|
\section{Pick your file}
|
||||||
|
|
||||||
vault is a single self-contained program. No installer, no runtime, no
|
vault is a single self-contained program. No installer, no runtime, no
|
||||||
@@ -714,13 +734,16 @@ vault.
|
|||||||
\begin{shell}
|
\begin{shell}
|
||||||
$ vault unlock # ask once, remember the key for 5 minutes
|
$ vault unlock # ask once, remember the key for 5 minutes
|
||||||
$ vault unlock --for 30m # or for as long as you say
|
$ 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
|
$ vault lock # forget it right now
|
||||||
\end{shell}
|
\end{shell}
|
||||||
|
|
||||||
After \cmd{unlock}, every command skips the passphrase prompt until the time
|
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
|
is up. Normally that's a hard deadline, counted from when you unlocked, so a
|
||||||
last used it, so a laptop left open still locks itself. Run \cmd{unlock}
|
laptop left open still locks itself; run \cmd{unlock} again for a fresh
|
||||||
again to start a fresh countdown.
|
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}
|
\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 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 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 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 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 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}
|
\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} \\
|
--yes & \textrm{---} & ask, for \cmd{migrate} \\
|
||||||
-r & \textrm{---} & \cmd{rm}, \cmd{get}, \cmd{cp}: one file; \cmd{ls}: one level \\
|
-r & \textrm{---} & \cmd{rm}, \cmd{get}, \cmd{cp}: one file; \cmd{ls}: one level \\
|
||||||
--for DURATION & \textrm{---} & \cmd{5m}, for \cmd{unlock} \\
|
--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 \\
|
-f & \textrm{---} & \cmd{rm}: ask before \cmd{-r}, fail if missing \\
|
||||||
\end{vtable}
|
\end{vtable}
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -5,6 +5,7 @@ vault-unlock, vault-lock \- remember the unlocked key for a while, or forget it
|
|||||||
.B vault unlock
|
.B vault unlock
|
||||||
.RB [ \-\-for
|
.RB [ \-\-for
|
||||||
.IR DURATION ]
|
.IR DURATION ]
|
||||||
|
.RB [ \-\-idle ]
|
||||||
.RB [ \-\-key
|
.RB [ \-\-key
|
||||||
.IR PATH ]
|
.IR PATH ]
|
||||||
.br
|
.br
|
||||||
@@ -17,8 +18,9 @@ asks for the passphrase, checks it, and starts a helper process, much like
|
|||||||
.BR ssh\-agent (1),
|
.BR ssh\-agent (1),
|
||||||
that holds the unlocked key in memory until \fIDURATION\fR has passed
|
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
|
(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
|
passphrase prompt. By default the deadline is fixed when you unlock and
|
||||||
not extend it. Running
|
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
|
.B unlock
|
||||||
again replaces the helper with a fresh one.
|
again replaces the helper with a fresh one.
|
||||||
.PP
|
.PP
|
||||||
@@ -38,6 +40,10 @@ handing the machine to anyone.
|
|||||||
.TP
|
.TP
|
||||||
.BI \-\-for " DURATION"
|
.BI \-\-for " DURATION"
|
||||||
How long to remember the key, such as 90s, 5m or 2h. Default 5m.
|
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
|
.PP
|
||||||
See also \fB\-\-key\fR in
|
See also \fB\-\-key\fR in
|
||||||
.BR vault (1).
|
.BR vault (1).
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ const usage = `Usage:
|
|||||||
vault rm -r FOLDER [-f] delete a folder and everything under it;
|
vault rm -r FOLDER [-f] delete a folder and everything under it;
|
||||||
asks first unless -f (or -rf) is given
|
asks first unless -f (or -rf) is given
|
||||||
vault unlock [--for DURATION] remember the passphrase for DURATION
|
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
|
vault lock forget it now
|
||||||
|
|
||||||
Options (any command):
|
Options (any command):
|
||||||
@@ -91,8 +92,8 @@ func main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
cmd, args := os.Args[1], os.Args[2:]
|
cmd, args := os.Args[1], os.Args[2:]
|
||||||
if cmd == agentCmd && len(args) == 2 {
|
if cmd == agentCmd && len(args) == 3 {
|
||||||
runAgent(args[0], args[1], os.Stdin, os.Stdout)
|
runAgent(args[0], args[1], args[2], os.Stdin, os.Stdout)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fset := flag.NewFlagSet("vault "+cmd, flag.ContinueOnError)
|
fset := flag.NewFlagSet("vault "+cmd, flag.ContinueOnError)
|
||||||
@@ -103,6 +104,7 @@ func main() {
|
|||||||
filler := fset.String("filler", "0", "")
|
filler := fset.String("filler", "0", "")
|
||||||
yes := fset.Bool("yes", false, "")
|
yes := fset.Bool("yes", false, "")
|
||||||
ttl := fset.Duration("for", 5*time.Minute, "")
|
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
|
// rm's -r and -f; Go's flag package has no combined short flags, so
|
||||||
// -rf and -fr are spelled out.
|
// -rf and -fr are spelled out.
|
||||||
recurse, force := fset.Bool("r", false, ""), fset.Bool("f", false, "")
|
recurse, force := fset.Bool("r", false, ""), fset.Bool("f", false, "")
|
||||||
@@ -295,10 +297,14 @@ func main() {
|
|||||||
check(err)
|
check(err)
|
||||||
id, err := unlockKeyFile(*keyPath, pass)
|
id, err := unlockKeyFile(*keyPath, pass)
|
||||||
check(err)
|
check(err)
|
||||||
check(agentStart(*keyPath, id, *ttl))
|
check(agentStart(*keyPath, id, *ttl, *idle))
|
||||||
id.Close()
|
id.Close()
|
||||||
|
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",
|
fmt.Fprintf(os.Stderr, "unlocked until %s; vault lock forgets the key sooner\n",
|
||||||
time.Now().Add(*ttl).Format("15:04:05"))
|
time.Now().Add(*ttl).Format("15:04:05"))
|
||||||
|
}
|
||||||
case "lock":
|
case "lock":
|
||||||
want(0, 0)
|
want(0, 0)
|
||||||
if agentStop(*keyPath) {
|
if agentStop(*keyPath) {
|
||||||
|
|||||||
Executable
+197
@@ -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 <andy@dixon.cx>
|
||||||
|
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/<letter>/<name>/, 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" <<EOF
|
||||||
|
[dixon]
|
||||||
|
name=repo.dixon.cx
|
||||||
|
baseurl=https://repo.dixon.cx/rpm
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgkey=https://repo.dixon.cx/dixon.asc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# pacman: one directory per architecture, each package signed, then
|
||||||
|
# repo-add (from an Arch container) and a signed database.
|
||||||
|
added=
|
||||||
|
for pkg in "$WORK"/out/*.pkg.tar.zst; do
|
||||||
|
a=$(basename "$pkg" .pkg.tar.zst)
|
||||||
|
a=${a##*-}
|
||||||
|
mkdir -p "$REPO_DIR/arch/$a"
|
||||||
|
cp "$pkg" "$REPO_DIR/arch/$a/"
|
||||||
|
sign --detach-sign -o "$REPO_DIR/arch/$a/$(basename "$pkg").sig" "$pkg"
|
||||||
|
added+=" $a"
|
||||||
|
done
|
||||||
|
for a in $(tr ' ' '\n' <<<"$added" | sort -u); do
|
||||||
|
dir=$REPO_DIR/arch/$a
|
||||||
|
# A fresh database of every package here; -p keeps the newest version.
|
||||||
|
rm -f "$dir/$ARCH_REPO".{db,files}{,.tar.gz}{,.sig}
|
||||||
|
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -v "$dir:/repo" -w /repo archlinux:latest \
|
||||||
|
sh -c "repo-add --quiet --prevent-downgrade $ARCH_REPO.db.tar.gz *.pkg.tar.zst" 2>&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"
|
||||||
+37
-1
@@ -21,6 +21,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/andydixon/vault/internal/pqcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKeyFile(t *testing.T) {
|
func TestKeyFile(t *testing.T) {
|
||||||
@@ -668,7 +670,7 @@ func TestAgent(t *testing.T) {
|
|||||||
secret = append(secret, id.Seed()...)
|
secret = append(secret, id.Seed()...)
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
stopped := make(chan struct{})
|
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" {
|
if msg, _ := io.ReadAll(pr); string(msg) != "ok" {
|
||||||
t.Fatalf("agent said %q", msg)
|
t.Fatalf("agent said %q", msg)
|
||||||
}
|
}
|
||||||
@@ -690,3 +692,37 @@ func TestAgent(t *testing.T) {
|
|||||||
t.Fatal("agent still running")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user