diff --git a/.gitignore b/.gitignore index 6cfe265..3c65cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ /vault.exe .claude/ /dist +/docs/guide/.tools/ +/docs/guide/version.tex +/docs/guide/vault-guide.pdf +/docs/guide/*.log diff --git a/CLAUDE.md b/CLAUDE.md index ec3e57c..2f0bb54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,8 @@ root that builds a single `vault` binary. Module `github.com/andydixon/vault`. - `lock*.go` - vault lock on `DIR/.lock`: fcntl on unix, LockFileEx on Windows, an exclusive lock file elsewhere. Writers lock exclusively, 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. - `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 05f7dca..0d60012 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ go test ./... both armv6 and armv7, and writes `dist/SHA256SUMS`. Set `TARGETS` to build a subset, for example `TARGETS="linux/amd64 darwin/arm64" ./build.sh`. +## User guide + +A full illustrated guide lives in `docs/guide`. Build the PDF with +`docs/guide/build-pdf.sh`, which fetches Tectonic if it isn't installed. +`docs/guide/shots.py` regenerates its screenshots from the real binary. + ## Use ```sh diff --git a/docs/guide/build-pdf.sh b/docs/guide/build-pdf.sh new file mode 100755 index 0000000..1908a51 --- /dev/null +++ b/docs/guide/build-pdf.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build the vault user guide PDF: docs/guide/vault-guide.pdf +# +# ./build-pdf.sh +# +# Uses Tectonic (a self-contained XeTeX engine). If `tectonic` is not on PATH, +# a pinned release is downloaded into docs/guide/.tools on first run. LaTeX +# packages and fonts are fetched by Tectonic and cached in your user cache. +# Screenshots are committed in img/; regenerate them with ./shots.py. +set -euo pipefail +cd "$(dirname "$0")" + +TECTONIC_VERSION=0.17.0 + +find_tectonic() { + if command -v tectonic >/dev/null; then command -v tectonic; return; fi + local local_bin=.tools/tectonic + if [[ -x $local_bin ]]; then echo "$local_bin"; return; fi + local asset + case "$(uname -s)/$(uname -m)" in + Linux/x86_64) asset=x86_64-unknown-linux-musl ;; + Linux/aarch64 | Linux/arm64) asset=aarch64-unknown-linux-musl ;; + Linux/armv7l) asset=arm-unknown-linux-musleabihf ;; + Darwin/x86_64) asset=x86_64-apple-darwin ;; + Darwin/arm64) asset=aarch64-apple-darwin ;; + *) echo "build-pdf: no Tectonic download for $(uname -s)/$(uname -m); install tectonic and retry" >&2; exit 1 ;; + esac + echo "build-pdf: fetching tectonic $TECTONIC_VERSION ($asset)" >&2 + mkdir -p .tools + curl -fsSL "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40$TECTONIC_VERSION/tectonic-$TECTONIC_VERSION-$asset.tar.gz" | + tar -xz -C .tools tectonic + echo "$local_bin" +} + +tectonic=$(find_tectonic) + +# The cover shows the git version and today's date. +version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) +printf '\\newcommand\\vaultversion{%s}\n\\newcommand\\vaultdate{%s}\n' \ + "$version" "$(date '+%B %Y')" >version.tex + +"$tectonic" --keep-logs --chatter minimal vault-guide.tex +echo "build-pdf: wrote $(pwd)/vault-guide.pdf" diff --git a/docs/guide/img/add.png b/docs/guide/img/add.png new file mode 100644 index 0000000..bc2f14f Binary files /dev/null and b/docs/guide/img/add.png differ diff --git a/docs/guide/img/build.png b/docs/guide/img/build.png new file mode 100644 index 0000000..d6828b9 Binary files /dev/null and b/docs/guide/img/build.png differ diff --git a/docs/guide/img/decoy-hidden.png b/docs/guide/img/decoy-hidden.png new file mode 100644 index 0000000..4073ba4 Binary files /dev/null and b/docs/guide/img/decoy-hidden.png differ diff --git a/docs/guide/img/decoy-views.png b/docs/guide/img/decoy-views.png new file mode 100644 index 0000000..8e4914f Binary files /dev/null and b/docs/guide/img/decoy-views.png differ diff --git a/docs/guide/img/errors.png b/docs/guide/img/errors.png new file mode 100644 index 0000000..bd51c07 Binary files /dev/null and b/docs/guide/img/errors.png differ diff --git a/docs/guide/img/genkey-decoy-prompt.png b/docs/guide/img/genkey-decoy-prompt.png new file mode 100644 index 0000000..d698d3e Binary files /dev/null and b/docs/guide/img/genkey-decoy-prompt.png differ diff --git a/docs/guide/img/genkey-prompt.png b/docs/guide/img/genkey-prompt.png new file mode 100644 index 0000000..5708db8 Binary files /dev/null and b/docs/guide/img/genkey-prompt.png differ diff --git a/docs/guide/img/genkey.png b/docs/guide/img/genkey.png new file mode 100644 index 0000000..62119b9 Binary files /dev/null and b/docs/guide/img/genkey.png differ diff --git a/docs/guide/img/get.png b/docs/guide/img/get.png new file mode 100644 index 0000000..cbb4830 Binary files /dev/null and b/docs/guide/img/get.png differ diff --git a/docs/guide/img/help.png b/docs/guide/img/help.png new file mode 100644 index 0000000..42cea1f Binary files /dev/null and b/docs/guide/img/help.png differ diff --git a/docs/guide/img/init.png b/docs/guide/img/init.png new file mode 100644 index 0000000..dd0c3d2 Binary files /dev/null and b/docs/guide/img/init.png differ diff --git a/docs/guide/img/list.png b/docs/guide/img/list.png new file mode 100644 index 0000000..9b2c984 Binary files /dev/null and b/docs/guide/img/list.png differ diff --git a/docs/guide/img/locking.png b/docs/guide/img/locking.png new file mode 100644 index 0000000..a7b7423 Binary files /dev/null and b/docs/guide/img/locking.png differ diff --git a/docs/guide/img/mv-rm.png b/docs/guide/img/mv-rm.png new file mode 100644 index 0000000..ea8c464 Binary files /dev/null and b/docs/guide/img/mv-rm.png differ diff --git a/docs/guide/img/on-disk.png b/docs/guide/img/on-disk.png new file mode 100644 index 0000000..0376474 Binary files /dev/null and b/docs/guide/img/on-disk.png differ diff --git a/docs/guide/img/scripting.png b/docs/guide/img/scripting.png new file mode 100644 index 0000000..31e5a8a Binary files /dev/null and b/docs/guide/img/scripting.png differ diff --git a/docs/guide/shots.py b/docs/guide/shots.py new file mode 100755 index 0000000..0a61260 --- /dev/null +++ b/docs/guide/shots.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +"""Regenerate the user guide's terminal screenshots from real vault runs. + +Every screenshot is the genuine output of the vault binary built from this +checkout, run in a throwaway home directory. The cosmetic touches are: the +sandbox path and file owner are shown as /home/sam and "sam sam", key lines +are highlighted in amber for the reader, and "moment" frames show a prompt +with a cursor, exactly as vault prints it before you type. + +Needs: go, python3, and Chrome or Chromium (set CHROME=/path if not found). +Output: docs/guide/img/*.png +""" + +import getpass +import glob +import grp +import html +import os +import shutil +import subprocess +import sys +import tempfile +import time +import urllib.request +import zipfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.abspath(os.path.join(HERE, "..", "..")) +IMG = os.path.join(HERE, "img") +TOOLS = os.path.join(HERE, ".tools") +FONT_URL = "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip" +SHOWN_HOME = "/home/sam" +PROMPT_USER = "sam@laptop" + + +def find_chrome(): + if os.environ.get("CHROME"): + return os.environ["CHROME"] + for name in ("chromium", "chromium-browser", "google-chrome", "google-chrome-stable"): + if shutil.which(name): + return shutil.which(name) + found = sorted(glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome"))) + mac = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + if found: + return found[-1] + if os.path.exists(mac): + return mac + sys.exit("shots: no Chrome or Chromium found; set CHROME=/path/to/chrome") + + +def font_dir(): + target = os.path.join(TOOLS, "jetbrains-mono") + if not os.path.exists(os.path.join(target, "JetBrainsMono-Regular.ttf")): + os.makedirs(target, exist_ok=True) + zpath = os.path.join(TOOLS, "jbm.zip") + print("shots: fetching JetBrains Mono") + urllib.request.urlretrieve(FONT_URL, zpath) + with zipfile.ZipFile(zpath) as z: + for n in z.namelist(): + base = os.path.basename(n) + if n.startswith("fonts/ttf/") and base in ("JetBrainsMono-Regular.ttf", "JetBrainsMono-Bold.ttf"): + with open(os.path.join(target, base), "wb") as f: + f.write(z.read(n)) + os.remove(zpath) + return target + + +class Sandbox: + """A fake home with vault on PATH. Commands run through bash.""" + + def __init__(self): + self.root = tempfile.mkdtemp(prefix="vault-shots-") + self.home = os.path.join(self.root, "sam") + self.bin = os.path.join(self.root, "bin") + os.makedirs(self.home) + os.makedirs(self.bin) + subprocess.run(["go", "build", "-o", os.path.join(self.bin, "vault"), "."], cwd=REPO, check=True) + + def env(self, extra=None): + e = { + "HOME": self.home, + "PATH": self.bin + os.pathsep + os.environ["PATH"], + "LANG": "C.UTF-8", + "TZ": "Europe/London", + } + e.update(extra or {}) + return e + + def run(self, command, passphrase=None, extra_env=None): + env = self.env(extra_env) + if passphrase is not None: + env["VAULT_PASSPHRASE"] = passphrase + p = subprocess.run(["bash", "-c", command], cwd=self.home, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + return self.tidy(p.stdout) + + def tidy(self, text): + owner = " %s %s " % (getpass.getuser(), grp.getgrgid(os.getgid()).gr_name) + return text.replace(self.home, SHOWN_HOME).replace(owner, " sam sam ").rstrip("\n") + + def write(self, rel, content): + path = os.path.join(self.home, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + mode = "wb" if isinstance(content, bytes) else "w" + with open(path, mode) as f: + f.write(content) + + +# ---- rendering ------------------------------------------------------------- + +CSS = """ +@font-face { font-family: JBM; src: url('file://%(fonts)s/JetBrainsMono-Regular.ttf'); } +@font-face { font-family: JBM; font-weight: bold; src: url('file://%(fonts)s/JetBrainsMono-Bold.ttf'); } +html, body { margin: 0; background: transparent; } +body { padding: 22px 48px 64px; display: flex; gap: 28px; align-items: flex-start; } +.win { width: %(width)dpx; border-radius: 12px; overflow: hidden; background: #0d1326; + box-shadow: 0 18px 40px rgba(6, 10, 30, .45), 0 0 0 1px rgba(255,255,255,.06); } +.bar { height: 34px; background: linear-gradient(#1b2340, #151c35); display: flex; + align-items: center; padding: 0 14px; position: relative; } +.dot { width: 12px; height: 12px; border-radius: 50%%; margin-right: 8px; } +.r { background: #ff5f57; } .y { background: #febc2e; } .g { background: #28c840; } +.title { position: absolute; left: 0; right: 0; text-align: center; color: #8b95b7; + font: 12.5px JBM, monospace; pointer-events: none; } +pre { margin: 0; padding: 16px 20px 18px; font: 14px/21px JBM, monospace; color: #d6ddf2; + white-space: pre; } +.u { color: #3ee0c5; font-weight: bold; } .p { color: #7aa2ff; font-weight: bold; } +.c { color: #ffffff; font-weight: bold; } .dim { color: #6f7a9c; } +.cur { background: #3ee0c5; color: #0d1326; } +.hl { color: #ffcc66; } +""" + + +def prompt(cwd="~"): + return ('%s %s $ ' + % (PROMPT_USER, html.escape(cwd))) + + +def window(lines, title, width): + body = "\n".join(lines) + return ('
' + '
%s
%s
' + % (html.escape(title), body)) + + +class Term: + """Accumulates one terminal window's contents.""" + + def __init__(self, box, title="sam@laptop: ~", width=860): + self.box, self.title, self.width, self.lines = box, title, width, [] + + def cmd(self, command, passphrase=None, extra_env=None, shown=None, cwd="~", highlight=()): + self.lines.append(prompt(cwd) + '%s' % html.escape(shown or command)) + out = self.box.run(command, passphrase, extra_env) + for line in out.split("\n") if out else []: + esc = html.escape(line) + if any(h in line for h in highlight): + esc = '%s' % esc + self.lines.append(esc) + return out + + def moment(self, command, prompt_text, cwd="~"): + """A frame captured while vault waits for typed input (no echo).""" + self.lines.append(prompt(cwd) + '%s' % html.escape(command)) + self.lines.append(html.escape(prompt_text) + ' ') + + def raw(self, text): + self.lines.append(text) + + def idle(self, cwd="~"): + self.lines.append(prompt(cwd) + ' ') + + def html(self): + return window(self.lines, self.title, self.width) + + def height(self): + return 34 + 16 + 18 + 21 * max(1, len(self.lines)) + + +def shoot(name, *terms): + chrome = find_chrome() + fonts = font_dir() + total_w = sum(t.width for t in terms) + 28 * (len(terms) - 1) + 96 + total_h = max(t.height() for t in terms) + 22 + 64 + page = "%s" % ( + CSS % {"fonts": fonts, "width": terms[0].width}, "".join(t.html() for t in terms)) + if len({t.width for t in terms}) > 1: + sys.exit("shots: side-by-side windows must share a width") + with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f: + f.write(page) + out = os.path.join(IMG, name + ".png") + subprocess.run([chrome, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--no-sandbox", + "--force-device-scale-factor=2", "--default-background-color=00000000", + "--window-size=%d,%d" % (total_w, total_h), "--screenshot=" + out, + "file://" + f.name], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + os.remove(f.name) + print("shots: wrote img/%s.png" % name) + + +# ---- scenes ---------------------------------------------------------------- + +def main(): + os.makedirs(IMG, exist_ok=True) + box = Sandbox() + P = "correct horse battery staple" + D = "shopping list 42" + + box.write("Documents/tax-return-2025.pdf", os.urandom(183_402)) + box.write("Documents/passport-scan.jpg", os.urandom(1_204_551)) + box.write("Documents/letters/landlord.txt", "Dear landlord,\n") + box.write("Documents/letters/bank.txt", "Dear bank,\n") + box.write("notes.md", "# ideas\n") + box.write("recipes/pancakes.txt", "flour, eggs, milk\n") + box.write("recipes/curry.txt", "spices\n") + + t = Term(box) + t.cmd("vault help") + shoot("help", t) + + t = Term(box) + t.moment("vault genkey", "Enter passphrase:") + shoot("genkey-prompt", t) + + t = Term(box) + t.moment("vault genkey", "Enter decoy passphrase (empty for none):") + shoot("genkey-decoy-prompt", t) + + t = Term(box) + t.cmd("vault genkey", passphrase=P, extra_env={"VAULT_DECOY_PASSPHRASE": D}) + t.cmd("ls -l ~/.vault/") + shoot("genkey", t) + + t = Term(box) + t.cmd("vault init") + t.cmd("ls -A ~/.vault/store/") + t.cmd("ls ~/.vault/store/records | wc -l") + shoot("init", t) + + t = Term(box) + t.cmd("vault add Documents/tax-return-2025.pdf", P) + t.cmd("vault add Documents/passport-scan.jpg id", P) + t.cmd("vault add Documents/letters", P) + t.cmd("vault add notes.md", P) + shoot("add", t) + + t = Term(box) + t.cmd("vault list", P) + t.cmd("vault list letters", P) + shoot("list", t) + + t = Term(box) + t.cmd("mkdir -p restored && cd restored && vault get id/passport-scan.jpg", P) + t.cmd("vault get tax-return-2025.pdf -o restored/tax.pdf", P) + t.cmd("vault get notes.md", P, highlight=("already exists",)) + shoot("get", t) + + t = Term(box) + t.cmd("echo '# more ideas' >> notes.md && vault add notes.md", P, highlight=("replaced",)) + t.cmd("vault mv notes.md ideas/notes.md", P) + t.cmd("vault rm letters/bank.txt", P) + t.cmd("vault list", P) + shoot("mv-rm", t) + + t = Term(box, title="sam@laptop: ~/.vault/store") + t.cmd("cd .vault/store && ls -l records | head -5", cwd="~/.vault/store", shown="ls -l records | head -5") + shoot("on-disk", t) + + # decoy: fill the decoy side, then show both views side by side + box.run("vault add recipes", D) + box.run("vault add notes.md shopping", D) + real = Term(box, title="real passphrase", width=600) + real.cmd("vault list", P) + decoy = Term(box, title="decoy passphrase", width=600) + decoy.cmd("vault list", D) + shoot("decoy-views", real, decoy) + + t = Term(box) + t.cmd("vault get ideas/notes.md -o /tmp/x.md", D, highlight=("not in the vault",)) + t.cmd("ls ~/.vault/store/records | wc -l") + shoot("decoy-hidden", t) + + # errors + t = Term(box) + t.cmd("vault list", "hunter2", highlight=("incorrect",)) + t.cmd("vault get does-not-exist.txt", P, highlight=("not in",)) + t.cmd("vault add notes.md ../escape", P, highlight=("invalid",)) + t.cmd("vault list --vault ~/Desktop", P, highlight=("not a vault",)) + t.cmd("vault init", highlight=("already",)) + shoot("errors", t) + + # locking: start a big add, then a second writer while it runs + box.write("big-video.mkv", os.urandom(600 << 20)) + first = Term(box, title="terminal 1", width=600) + second = Term(box, title="terminal 2", width=600) + env = box.env({"VAULT_PASSPHRASE": P}) + a = subprocess.Popen(["bash", "-c", "vault add big-video.mkv"], cwd=box.home, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + time.sleep(1.5) + second.cmd("vault add recipes/curry.txt", P, highlight=("waiting",)) + first.lines.append(prompt() + 'vault add big-video.mkv') + first.lines += [html.escape(box.tidy(l)) for l in a.communicate()[0].rstrip("\n").split("\n")] + shoot("locking", first, second) + os.remove(os.path.join(box.home, "big-video.mkv")) + + # automation + t = Term(box) + t.cmd("export VAULT_PASSPHRASE='correct horse battery staple'", shown="export VAULT_PASSPHRASE=…") + t.cmd("vault list ideas", P) + t.cmd("vault get nope 2>/dev/null; echo \"exit code: $?\"", P) + shoot("scripting", t) + + # building, into a temporary OUT so an existing dist/ is left alone + rel = tempfile.mkdtemp(prefix="vault-release-") + targets = "linux/amd64 linux/arm darwin/arm64 windows/amd64" + t = Term(box, title="sam@laptop: ~/src/vault") + out = subprocess.run(["./build.sh"], cwd=REPO, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=dict(os.environ, TARGETS=targets, OUT=rel)).stdout + sums = subprocess.run(["sha256sum", "-c", "SHA256SUMS"], cwd=rel, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout + t.lines.append(prompt("~/src/vault") + 'export TARGETS="%s"' % targets) + t.lines.append(prompt("~/src/vault") + 'OUT=/tmp/release ./build.sh') + t.lines += [html.escape(l.replace(rel, "/tmp/release")) for l in out.rstrip("\n").split("\n")] + t.lines.append(prompt("~/src/vault") + 'cd /tmp/release && sha256sum -c SHA256SUMS') + t.lines += [html.escape(l) for l in sums.rstrip("\n").split("\n")] + shoot("build", t) + shutil.rmtree(rel, ignore_errors=True) + + shutil.rmtree(box.root, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/docs/guide/vault-guide.tex b/docs/guide/vault-guide.tex new file mode 100644 index 0000000..48580c7 --- /dev/null +++ b/docs/guide/vault-guide.tex @@ -0,0 +1,1217 @@ +% The vault field guide. Build with ./build-pdf.sh +\documentclass[10pt,oneside,openany]{book} +\usepackage{vaultguide} +\InputIfFileExists{version.tex}{}{\newcommand\vaultversion{dev}\newcommand\vaultdate{\today}} + +\hypersetup{pdftitle={vault: the field guide},pdfauthor={The vault Authors}, + pdfsubject={Post-quantum file encryption with a decoy passphrase}} + +\begin{document} + +\frontmatter +\makecover +\clearpage + +% ====================================================================== +\chapter*{Hi. Start here.} +\markboth{Start here}{} +\addcontentsline{toc}{chapter}{Start here} + +This guide is built for skimming. Genuinely. You are allowed to jump +around, read the boxes and ignore the paragraphs, or read chapter 5 first +because that's the one with the commands in it. Nobody's checking. + +Every chapter opens with a \hl{TL;DR}. Read that and you've got the gist. The +rest is there for when you want the detail, or when something goes wrong at +11pm and you need to know \emph{exactly} what a message means. + +\section*{The boxes, decoded} + +\begin{tldr} +The whole chapter in about three lines. If you only read one thing, read this. +\end{tldr} + +\begin{quickwin} +A small thing you can do right now that makes life better. +\end{quickwin} + +\begin{headsup} +Something that will trip you up if you're not expecting it. +\end{headsup} + +\begin{danger} +Something that can lose you data. These are rare. Read every one of them. +\end{danger} + +\begin{squirrel} +A tangent. Interesting, possibly, but you can skip it and lose nothing. +\end{squirrel} + +\begin{whyso} +The reasoning behind a design decision, for the curious. +\end{whyso} + +\begin{pocket} +The end-of-chapter recap. A checklist of the things worth keeping in your head. +\end{pocket} + +\section*{The absolute fastest route} + +Got two minutes? Here's all of it: + +\begin{shell} +$ vault genkey # make your key (pick a good passphrase) +$ vault init # make your vault +$ vault add taxes.pdf # put something in +$ vault list # see what's in there +$ vault get taxes.pdf # get it back out +\end{shell} + +That's the tool. Everything after this page is detail, safety rails and the +fun decoy trick in chapter 6. \faIcon[regular]{smile} + +{\hypersetup{linkcolor=vtext}\tableofcontents} + +\mainmatter + +% ====================================================================== +\chapter{Meet vault} +\tagline{what it is, what it isn't, and a 30-second mental model} + +\begin{tldr} +vault is one small program that keeps your files encrypted in a folder. It +uses post-quantum cryptography, hides your file names and sizes, and has an +optional decoy passphrase that shows a harmless set of files instead of the +real ones. +\end{tldr} + +\section{What it actually does} + +You give vault a file. It encrypts the file and drops the result into your +\emph{vault}, which is just a folder. Later you ask for the file back and vault +decrypts it. That's the core loop. Put in, take out. + +The interesting part is what the folder looks like from the outside. Nothing. +Well, not \emph{nothing}. A pile of randomly named files, all dated 1 January +1970, in a handful of suspiciously round sizes. No names. No real sizes. No +dates. No folders. Someone who steals the folder learns roughly how much stuff +you have, and that's about it. + +\begin{center} +\begin{tikzpicture}[ + doc/.style={draw=vsky,fill=vsky!8,rounded corners=2pt,minimum width=17mm,minimum height=8mm,font=\scriptsize\ttfamily}, + blob/.style={draw=vteal,fill=vteal!10,rounded corners=2pt,minimum width=24mm,minimum height=6.5mm,font=\scriptsize\ttfamily}, + lbl/.style={font=\scriptsize\labelfont,text=vslate}] + \node[doc] (a) at (0,1.1) {taxes.pdf}; + \node[doc] (b) at (0,0) {passport.jpg}; + \node[doc] (c) at (0,-1.1) {diary.md}; + \node[lbl] at (0,2) {YOUR FILES}; + \node[circle,fill=vnavy,minimum size=19mm,text=white,font=\Large] (v) at (4,0) {\faIcon{lock}}; + \node[font=\scriptsize\displaybold,text=vnavy] at (4,-1.45) {vault}; + \node[font=\tiny,text=vslate,align=center] at (4,-1.85) {passphrase + key}; + \node[blob] (r1) at (8.2,1.3) {3fa9c1\ldots\ 1970}; + \node[blob] (r2) at (8.2,0.45) {b07e2d\ldots\ 1970}; + \node[blob] (r3) at (8.2,-0.4) {91c4aa\ldots\ 1970}; + \node[blob] (r4) at (8.2,-1.25) {e5580f\ldots\ 1970}; + \node[lbl] at (8.2,2) {WHAT A THIEF SEES}; + \foreach \n in {a,b,c} \draw[-{Stealth[length=2.5mm]},vslate,thick] (\n.east) -- (v.west); + \foreach \n in {r1,r2,r3,r4} \draw[-{Stealth[length=2.5mm]},vteal,thick] (v.east) -- (\n.west); +\end{tikzpicture} +\end{center} + +\section{The three things that make it different} + +\subsection{It's post-quantum} +Most encryption you use today would, in theory, fall to a big enough quantum +computer. Nobody has one yet. But someone could copy your encrypted files +\emph{today} and sit on them until they do. It's called \emph{harvest now, +decrypt later}, and it's a real strategy. + +vault uses two post-quantum algorithms at once: \hl{FrodoKEM-640} and +\hl{ML-KEM-768}. An attacker has to break \emph{both}. Chapter 9 has the gory +details. + +\subsection{It hides metadata, not just contents} +Plenty of tools encrypt what's \emph{in} your files but happily leave +\file{divorce-papers-final-v3.pdf} sitting there in plain sight. vault encrypts +the names too. And the sizes. And the timestamps. + +\subsection{It can lie for you} +Optionally, your key can have a second, \hl{decoy} passphrase. Type it, and +vault shows a completely separate set of files. Your real files don't appear, +don't error, don't leave a hint. They just aren't there, as far as anyone +watching can tell. That's chapter 6, and yes, it's the fun one. + +\section{What vault is \emph{not}} + +Honesty time. Knowing the edges matters as much as knowing the middle. + +\begin{nopes} + \item \textbf{Not a sync service.} It's a folder. You can sync the folder + yourself (chapter 8), but vault doesn't do it for you. + \item \textbf{Not a backup by itself.} One copy of anything is zero copies. + Back up the vault folder \emph{and} your key. + \item \textbf{Not magic against malware.} If something nasty is running on + your computer while you type your passphrase, it can watch you type + it. No file-encryption tool fixes a compromised machine. + \item \textbf{Not recoverable without the passphrase.} There is no reset + link. There's no ``forgot password''. That's the whole point, and it + cuts both ways. +\end{nopes} + +\begin{squirrel} +Why is it called \emph{vault} and not something cooler? Because you type it a +lot, and five letters you already know how to spell beat a clever name every +single time. Also, all the cool names were taken. +\end{squirrel} + +\begin{pocket} +\begin{pocketlist} + \item vault encrypts files into a folder and hides their names, sizes and dates. + \item The encryption is post-quantum: two algorithms, attacker must break both. + \item An optional decoy passphrase shows a different, harmless set of files. + \item Lose the passphrase and the files are gone. No reset. Ever. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Install it} +\tagline{grab a binary, check it, put it on your PATH} + +\begin{tldr} +Download the one file for your system, check its SHA-256, rename it to +\cmd{vault} (or \cmd{vault.exe}), and put it somewhere on your \env{PATH}. +Or build every platform yourself with \cmd{./build.sh}. +\end{tldr} + +\section{Pick your file} + +vault is a single self-contained program. No installer, no runtime, no +dependencies. You just need the right file for your operating system and CPU. + +\begin{vtable}{>{\semibold}p{34mm}Y>{\ttfamily\small}p{50mm}} +\thead{YOU HAVE} & \thead{WHICH MEANS} & \thead{DOWNLOAD} \\ \midrule +Windows PC & most laptops and desktops & vault-windows-amd64.exe \\ +Windows on ARM & Surface Pro X, Snapdragon laptops & vault-windows-arm64.exe \\ +Mac, 2020 or later & Apple silicon (M1, M2, M3\ldots) & vault-darwin-arm64 \\ +Mac, older & Intel & vault-darwin-amd64 \\ +Linux PC or server & almost everything & vault-linux-amd64 \\ +Linux on ARM64 & Pi 4/5 (64-bit OS), Graviton, Ampere & vault-linux-arm64 \\ +Raspberry Pi, 32-bit & Pi 2, 3, 4 on a 32-bit OS & vault-linux-armv7 \\ +Raspberry Pi Zero / 1 & the tiny older ones & vault-linux-armv6 \\ +Android & in Termux & vault-android-arm64 \\ +FreeBSD, OpenBSD, NetBSD & amd64, arm64, 386, arm\ldots & vault-freebsd-amd64 etc. \\ +Something exotic & RISC-V, POWER, s390x, MIPS, LoongArch & see \cmd{dist/} \\ +\end{vtable} + +\begin{squirrel} +The full release covers 43 platform builds, including AIX, Solaris, illumos, +Plan~9 and even WebAssembly. Will you ever need \file{vault-plan9-arm}? +Almost certainly not. Is it pleasing that it exists? Extremely. +\end{squirrel} + +\begin{headsup} +Not sure which Linux you have? Run \cmd{uname -m}. \cmd{x86\_64} means amd64, +\cmd{aarch64} means arm64, \cmd{armv7l} means armv7, and \cmd{armv6l} means armv6. +\end{headsup} + +\section{Check the file hasn't been tampered with} + +Every release comes with a \file{SHA256SUMS} file. It takes ten seconds to +check, and it's the difference between running \emph{vault} and running +\emph{something pretending to be vault}. For an encryption tool, that matters. + +\begin{shell} +# Linux +$ sha256sum -c SHA256SUMS --ignore-missing + +# macOS +$ shasum -a 256 -c SHA256SUMS --ignore-missing + +# Windows (PowerShell): compare the output with the line in SHA256SUMS +PS> Get-FileHash .\vault-windows-amd64.exe -Algorithm SHA256 +\end{shell} + +You want to see \cmd{OK}. Anything else: stop, and download it again from +the source you trust. + +\section{Put it on your PATH} + +\subsection{Linux and macOS} +\begin{shell} +$ chmod +x vault-linux-amd64 +$ sudo mv vault-linux-amd64 /usr/local/bin/vault +$ vault help +\end{shell} + +No admin rights? Use \file{\tld/.local/bin} instead, which most modern +distributions already have on the \env{PATH}. + +\begin{headsup} +\textbf{macOS says the file ``cannot be opened''?} It's not signed by Apple, +so Gatekeeper quarantines it. Once you've checked the SHA-256, clear the flag: +\cmd{xattr -d com.apple.quarantine /usr/local/bin/vault}. +\end{headsup} + +\subsection{Windows} +\begin{steps} + \item Make a folder, say \file{C:\textbackslash Tools}, and move the file in, + renamed to \file{vault.exe}. + \item Start menu, search for \hl{``environment variables''}, open + \hl{Edit the environment variables for your account}. + \item Edit \hl{Path}, add \file{C:\textbackslash Tools}, and click OK. + \item Open a \emph{new} terminal and run \cmd{vault help}. +\end{steps} + +\subsection{Android (Termux)} +\begin{shell} +$ curl -LO https://.../vault-android-arm64 +$ chmod +x vault-android-arm64 && mv vault-android-arm64 $PREFIX/bin/vault +\end{shell} + +\section{Did it work?} + +Run \cmd{vault help}. You should see the full command list: + +\shot{help}{\cmd{vault help}: everything vault can do, on one screen} + +\section{Building it yourself} + +Prefer to build from source? You need Go 1.26.6 or newer. Then: + +\begin{shell} +$ go build . # just your platform, produces ./vault +$ ./build.sh # every platform, into ./dist, with SHA256SUMS +\end{shell} + +\cmd{build.sh} cross-compiles every target Go supports without C libraries. +32-bit ARM Linux gets two builds, armv6 and armv7. Want just a few? Set +\env{TARGETS}. Want them somewhere else? Set \env{OUT}. + +\shot{build}{A partial build, followed by checking every binary against SHA256SUMS} + +\begin{whyso} +The binaries are \emph{static} and \emph{reproducible}: no C code, no system +libraries, file paths stripped, build IDs zeroed. Two people building the same +commit get byte-identical files, so you can check a release against your own +build. +\end{whyso} + +\begin{pocket} +\begin{pocketlist} + \item One file per platform. Rename it \cmd{vault} and put it on your \env{PATH}. + \item Always check the SHA-256 against \file{SHA256SUMS}. + \item macOS: clear the quarantine flag after checking. + \item \cmd{./build.sh} builds all 43 platform binaries from source. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Your key} +\tagline{the one file that matters most, and the passphrase that guards it} + +\begin{tldr} +\cmd{vault genkey} creates your key at \file{\tld/.vault/key}, protected by a +passphrase. It can also set a decoy passphrase. \textbf{Back the key file up.} +Without it, and its passphrase, your files cannot be recovered by anyone. +\end{tldr} + +\section{Making one} + +\begin{shell} +$ vault genkey +\end{shell} + +vault asks for a passphrase, then asks you to type it again. Nothing appears +on screen as you type. That's deliberate, not broken. + +\shot{genkey-prompt}{Waiting for your passphrase. Typing is invisible on purpose.} + +Next it offers the \hl{decoy passphrase}. Press \keycap{Enter} to skip it, or +set one now. We'll cover what it's for in chapter 6. The short version: it +opens a separate, fake-but-plausible set of files. + +\shot{genkey-decoy-prompt}{The optional decoy passphrase. An empty answer means ``no decoy''.} + +\begin{headsup} +\textbf{The decoy can only be set here, when the key is made.} There's no +``add a decoy later'' command. If you think you might ever want one, set one +now. It costs nothing, and nobody can tell it's there. +\end{headsup} + +When it's done, you get one line and one small file: + +\shot{genkey}{Your key: 319 bytes, readable only by you} + +\section{Choosing a passphrase that doesn't suck} + +The key file is encrypted with your passphrase using \hl{Argon2id}, a +deliberately slow, memory-hungry function. Each guess costs an attacker real +time and 64\,MiB of memory. Slow guessing only helps, though, if there are a lot +of guesses to make. + +\begin{checks} + \item \textbf{Use four or more random words.} \emph{correct horse battery + staple} beats \emph{P@ssw0rd!} by a mile, and you can actually remember it. + \item \textbf{Make it unique.} Not your email password. Not your bank's. + \item \textbf{Make the decoy believable.} It should feel like something + you'd genuinely pick. More in chapter 6. +\end{checks} + +\begin{quickwin} +Use a password manager's \emph{passphrase} generator (most have one) to make +the words, then practise typing it five times. Muscle memory is a legitimate +security tool. +\end{quickwin} + +\section{Where it lives} + +By default your key is \file{\tld/.vault/key}. On Windows, that's +\file{\%USERPROFILE\%\textbackslash.vault\textbackslash key}. Want it +somewhere else, like a USB stick? Two options: + +\begin{shell} +$ vault genkey --key /media/usb/vault.key # this once +$ export VAULT_KEY=/media/usb/vault.key # every command from now on +\end{shell} + +\begin{squirrel} +Keeping the key on a USB stick and the vault on your laptop is a neat trick. +Pull the stick out and the vault is inert, even if someone knows your +passphrase. Two things to steal instead of one. +\end{squirrel} + +\section{Back. It. Up.} + +\begin{danger} +\textbf{If you lose the key file, or forget the passphrase, your files are +gone.} Not ``hard to get back''. Gone. vault has no recovery mechanism, no +master key, no back door. That's what makes it trustworthy, and it's also +what makes a lost key final. +\end{danger} + +The key file is encrypted, so a copy is safe to keep somewhere a little less +private than your laptop. Some decent options: + +\begin{itemize} + \item A USB stick in a drawer. Or two, in two different drawers. + \item Your password manager, as an attached file. + \item Printed out. It's only a few lines of text. Paper survives surprisingly well. +\end{itemize} + +\begin{whyso} +\textbf{What's actually in the key file?} Two equal-sized encrypted slots, in +random order. Each passphrase opens exactly one. When there's no decoy, the +spare slot is just random bytes, and nobody can tell random bytes from an +encrypted slot. So the file gives away nothing about whether a decoy exists. +\end{whyso} + +\begin{pocket} +\begin{pocketlist} + \item \cmd{vault genkey} makes \file{\tld/.vault/key}. It won't overwrite an existing key. + \item Four-plus random words. Unique. Practise it. + \item Decide on a decoy now. It can't be added later. + \item Back up the key file. Somewhere else. Today. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Your first vault} +\tagline{one command, and a peek inside} + +\begin{tldr} +\cmd{vault init} creates an empty vault at \file{\tld/.vault/store}. It's +just a folder. Inside it seeds some random filler, which is normal and part of +the decoy trick. +\end{tldr} + +\section{Create it} + +\begin{shell} +$ vault init +\end{shell} + +That's it. No passphrase needed, because an empty vault has nothing to +protect yet. + +\shot{init}{A fresh vault. Two sub-folders, a marker, and\ldots\ records already?!} + +\section{Wait, why is there already stuff in it?} + +Good catch. A brand new vault holds somewhere between 8 and 32 entries of +random filler. They're encrypted to a key that was thrown away the moment +it was used, so \emph{nobody} can open them. Not you, not us, not anyone. + +It looks weird, but it's what makes the decoy passphrase believable. Every +vault, decoy or not, always contains entries you can't open. So when someone +with your decoy passphrase sees unopenable entries, that's completely +unremarkable. It's just filler. (Some of it might not be. They can't tell.) + +\begin{headsup} +The filler takes a few tens of megabytes. That's the price of plausible +deniability, and it's paid once. +\end{headsup} + +\section{More than one vault} + +Want a separate vault for work stuff? Point \cmd{--vault} anywhere: + +\begin{shell} +$ vault init --vault ~/work-vault +$ vault add contract.pdf --vault ~/work-vault +\end{shell} + +Or set \env{VAULT\_DIR} so every command uses it. The same key works with any +number of vaults. + +\begin{quickwin} +Add \cmd{export VAULT\_DIR=\ldots} to your shell profile and you'll never +type \cmd{--vault} again. +\end{quickwin} + +\begin{pocket} +\begin{pocketlist} + \item \cmd{vault init} makes \file{\tld/.vault/store}. \cmd{--vault DIR} to put it elsewhere. + \item Filler entries are normal. Nobody can open them. That's the point. + \item One key, as many vaults as you like. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Everyday moves} +\tagline{add, list, get, mv, rm: the five commands you'll actually use} + +\begin{tldr} +\cmd{add} puts files in (optionally into a folder), \cmd{list} shows them, +\cmd{get} takes one back out, \cmd{mv} renames, \cmd{rm} deletes. Each asks +for your passphrase. That's the whole daily workflow. +\end{tldr} + +\section{Putting things in: \texttt{add}} + +\begin{shell} +$ vault add FILE-OR-FOLDER [FOLDER-IN-VAULT] +\end{shell} + +Give it a file, and it goes in under its own name. Give it a second argument, +and it goes into that folder inside the vault. Give it a whole folder, and +everything inside comes along too, subfolders and all. + +\shot{add}{Adding a file, a file into a folder, a whole folder, and another file} + +Notice what happened there: + +\begin{itemize} + \item \file{Documents/tax-return-2025.pdf} became plain \file{tax-return-2025.pdf}. + vault keeps the \emph{file name}, not the path you typed. + \item With a second argument, \file{id}, the passport scan landed in + \file{id/passport-scan.jpg}. + \item Adding the folder \file{Documents/letters} kept its own name and + structure: \file{letters/bank.txt}, \file{letters/landlord.txt}. +\end{itemize} + +\begin{headsup} +\textbf{Folders inside a vault aren't real folders.} They're just part of each +file's name, like \file{letters/bank.txt}. So there's no \cmd{mkdir}, and a +folder disappears when its last file goes. No housekeeping required. +\end{headsup} + +\subsection{Adding something that's already there} +Adding a name that already exists \hl{replaces} it, and vault tells you so. +The old version is deleted \emph{after} the new one is safely stored, so a +crash halfway through never leaves you with neither. + +\subsection{What gets skipped} +When adding a folder, anything that isn't a plain file, such as symbolic +links, sockets or devices, is skipped with a note. Files larger than 1\,TiB are +refused. (If you have a single 1\,TiB file, we have questions.) + +\section{Seeing what's inside: \texttt{list}} + +\begin{shell} +$ vault list # everything +$ vault list letters # just what's inside letters/ +\end{shell} + +Each line shows the size, the file's \emph{own} last-modified time from when +you added it, and its name, sorted alphabetically. + +\shot{list}{The full catalogue, then just one folder} + +\begin{whyso} +\textbf{Why does \texttt{list} need my passphrase?} Because the names are +encrypted too. Without the key, vault can't read the catalogue any better +than a thief can. +\end{whyso} + +\section{Getting things out: \texttt{get}} + +\begin{shell} +$ vault get NAME # writes the file here, under its own name +$ vault get NAME -o OUTPUT # writes it wherever you say +\end{shell} + +\shot{get}{Getting files back, including vault refusing to overwrite} + +Three safety habits are built in, so you don't need to think about them: + +\begin{checks} + \item \textbf{It never overwrites.} If the output exists, vault stops. Move + the old one or use \cmd{-o}. + \item \textbf{Nothing appears until everything checks out.} vault writes to a + hidden temporary file, verifies every single byte, and only + then gives it its real name. A tampered or damaged file never shows up + half-written. + \item \textbf{It restores the details.} You get back the original permissions + and modification time. +\end{checks} + +\section{Renaming and moving: \texttt{mv}} + +\begin{shell} +$ vault mv OLD NEW +\end{shell} + +\cmd{NEW} is the \emph{full} new name, folder included. So to move +\file{notes.md} into \file{ideas}, it's \cmd{vault mv notes.md +ideas/notes.md}, not \cmd{\ldots\ ideas/}. It won't overwrite an existing name, +and it doesn't re-encrypt the file, so it's instant however big the file is. + +\section{Deleting: \texttt{rm}} + +\begin{shell} +$ vault rm NAME +\end{shell} + +Removes the file's catalogue entry and its encrypted contents. One file per +command, on purpose. There's no recursive delete to fat-finger. + +\shot{mv-rm}{Replace, move, delete, and the result} + +\begin{danger} +\textbf{\texttt{rm} is immediate and there's no undo.} No recycle bin, no +``are you sure?''. If it matters, make sure it's backed up somewhere first. +\end{danger} + +\begin{squirrel} +Need to delete a whole folder? Loop over \cmd{list}. The name starts at +character 31 of each line, so:\\ +\cmd{vault list old | cut -c31- | while read -r f; do vault rm "\$f"; done}\\ +With \env{VAULT\_PASSPHRASE} set, or it'll ask every time. See chapter 7. +\end{squirrel} + +\section{Flags work anywhere} + +Every command understands \cmd{--key PATH} and \cmd{--vault DIR}, and they can +go before, between or after the other arguments. These all do the same thing: + +\begin{shell} +$ vault add --vault ~/work report.pdf +$ vault add report.pdf --vault ~/work +$ vault add report.pdf --vault=~/work +\end{shell} + +\begin{pocket} +\begin{pocketlist} + \item \cmd{add FILE [FOLDER]}: keeps the file's name, optionally in a folder. Folders add recursively. + \item Adding an existing name replaces it, safely. + \item \cmd{list [FOLDER]}, \cmd{get NAME [-o OUT]}, \cmd{mv OLD NEW}, \cmd{rm NAME}. + \item \cmd{get} never overwrites and never publishes a half-checked file. + \item \cmd{rm} has no undo. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{The decoy passphrase} +\tagline{one key, two passphrases, two completely different vaults} + +\begin{tldr} +Set a decoy passphrase when you run \cmd{vault genkey}. Every command run with +the decoy sees only decoy files. Your real files are invisible to it: they +don't list, can't be fetched, and look exactly like the random filler every +vault has. Make the decoy believable, and keep it at least as big as the real +thing. +\end{tldr} + +\section{The idea} + +Sometimes ``I won't tell you my passphrase'' isn't an option. A border +crossing. A pushy partner. Someone standing over you. The decoy passphrase is +for that moment. You type it, the vault opens, and it's full of genuinely +private-looking stuff: bank letters, some personal notes, a few scans. +Everything behaves normally. It's just not your \emph{real} stuff. + +\section{How it looks} + +Here's one vault, opened twice, with the two different passphrases: + +\shot[1.2]{decoy-views}{Same key file, same vault folder, two passphrases, two worlds} + +Nothing on the right hints that the left exists. Ask the decoy for a real +file and you get the same message you'd get for a typo. The folder's entry +count doesn't give it away either, because filler is always there: + +\shot{decoy-hidden}{The decoy can't see real files, and a few dozen entries on disk is unremarkable} + +\begin{center} +\begin{tikzpicture}[x=5.4mm,y=5.4mm] + \def\cells{FRDFFRDRFFDRFDFRRFDFFRDFRDFFRFDRFDFFRDRF} + \newcommand\grid[4]{% x offset, title, colour for R, colour for D + \node[font=\scriptsize\labelfont,text=vslate,anchor=south] at (#1+3.5,5.35) {#2}; + \foreach \i in {0,...,39}{ + \pgfmathtruncatemacro\cx{mod(\i,8)}\pgfmathtruncatemacro\ry{\i/8} + \StrChar{\cells}{\numexpr\i+1\relax}[\kind] + \if\kind R\def\col{#3}\else\if\kind D\def\col{#4}\else\def\col{vline}\fi\fi + \fill[\col,rounded corners=1pt] (#1+\cx,4-\ry) rectangle ++(0.82,0.82);}} + \grid{0}{ON DISK}{vline}{vline} + \grid{10}{REAL PASSPHRASE}{vteal}{vline} + \grid{20}{DECOY PASSPHRASE}{vline}{vviolet} +\end{tikzpicture}\\[2pt] +{\footnotesize\color{vslate}Every square is an encrypted entry. Each passphrase lights up only its own. Grey is filler, or the other side.} +\end{center} + +\section{Setting it up} + +The decoy is chosen when the key is made, and only then: + +\begin{steps} + \item Run \cmd{vault genkey}. Type your \emph{real} passphrase, twice. + \item At \hl{Enter decoy passphrase (empty for none)}, type the decoy. Twice. + \item Run \cmd{vault init} as normal. + \item Add some decoy files, typing the \emph{decoy} passphrase when asked. + \item Add your real files, typing the \emph{real} passphrase. +\end{steps} + +The two must be different (vault checks). In scripts, set +\env{VAULT\_DECOY\_PASSPHRASE} alongside \env{VAULT\_PASSPHRASE} before +\cmd{genkey}. Chapter 7 has more on scripting. + +\begin{headsup} +\textbf{Already have a key without a decoy?} You'll need a new key: run +\cmd{genkey} with a different \cmd{--key}, make a new vault, \cmd{get} +everything out of the old one and \cmd{add} it to the new. The key file +format deliberately gives no way to tell whether a decoy slot is in use, so +there's nothing to safely ``upgrade''. +\end{headsup} + +\section{Why it holds up} + +\begin{itemize} + \item \textbf{The key file can't tell.} It always has two slots of identical + size in random order. With no decoy, one slot is random noise, which + looks the same as an encrypted slot. Both are tried every time, so + even the timing is the same. + \item \textbf{The vault can't tell.} Real and decoy entries are sealed to + different keys, but they're the same size, the same shape, and all + dated 1970. Nothing on disk says which key an entry belongs to. + \item \textbf{The filler covers for you.} Every vault starts with 8 to 32 + entries nobody can open. So ``some entries I can't open'' is true of + \emph{every} vault, decoy or not. + \item \textbf{The errors are identical.} A real file asked for by name with + the decoy passphrase gives exactly the same ``not in the vault'' as + a typo. +\end{itemize} + +\section{Making it believable: the playbook} + +A decoy only works if it looks like the real thing. An empty decoy vault, or +one with two files called \file{test.txt}, is a neon sign. + +\begin{checks} + \item \textbf{Fill it with plausibly private stuff.} Bank letters, payslips, + a diary-ish notes file, scans of documents. The kind of thing someone + \emph{would} encrypt. + \item \textbf{Keep it bigger than your real collection}, or at least as big. + See the next section for why. + \item \textbf{Keep it alive.} Add to it now and then, so the dates in + \cmd{list} look like a real person uses it. + \item \textbf{Know it cold.} Practise the decoy as much as the real one. + Hesitating on the ``real'' passphrase is a tell. + \item \textbf{Never mention it.} Not in notes, not in a README, not + in a script sitting on the same machine. +\end{checks} + +\section{The honest limits} + +\begin{headsup} +\textbf{Numbers can give you away.} Every file is one entry (one file in +\file{records/}), and filler adds at most 32. If the decoy shows 3 files but +\file{records/} holds 400, the filler can't explain the difference. Keep the decoy collection +large enough that the leftovers look like filler. +\end{headsup} + +\begin{headsup} +\textbf{Growth over time is visible.} If someone copies your vault folder +today and again next month, they can see it grew, even if the decoy didn't +change. They can't see \emph{what} was added. They can see \emph{that} +something was. +\end{headsup} + +\begin{nopes} + \item It won't help if someone saw you type the real passphrase, or has + malware on your machine. + \item It doesn't stop anyone deleting the raw files. It hides; it doesn't guard. + \item It isn't legal advice. Some places treat misleading an official as an + offence in its own right. Know where you are. +\end{nopes} + +\begin{pocket} +\begin{pocketlist} + \item The decoy is set at \cmd{genkey} time only. + \item Decoy and real are two separate worlds in one folder. Neither can see the other. + \item Filler, identical entry sizes and a two-slot key file make the hiding work. + \item Keep the decoy big, believable and alive. Keep quiet about it. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Scripts and automation} +\tagline{environment variables, exit codes, and a few recipes} + +\begin{tldr} +Set \env{VAULT\_PASSPHRASE}, \env{VAULT\_KEY} and \env{VAULT\_DIR} and vault +never asks anything. Output you'd pipe (\cmd{list}, \cmd{help}) goes to +standard output; everything else goes to standard error. Exit code 0 means +it worked. +\end{tldr} + +\section{The environment variables} + +\begin{vtable}{>{\leavevmode\ttfamily\small\color{vvioletdeep}}p{50mm}Y} +\thead{VARIABLE} & \thead{WHAT IT DOES} \\ \midrule +VAULT\_PASSPHRASE & The passphrase. Skips the prompt. With \cmd{genkey}, it's the new key's passphrase. \\ +VAULT\_DECOY\_PASSPHRASE & \cmd{genkey} only: the decoy passphrase. If \env{VAULT\_PASSPHRASE} is set and this isn't, no decoy is made. \\ +VAULT\_KEY & The key file. Default \file{\tld/.vault/key}. Same as \cmd{--key}. \\ +VAULT\_DIR & The vault folder. Default \file{\tld/.vault/store}. Same as \cmd{--vault}. \\ +\end{vtable} + +\shot{scripting}{With the variable set, no prompts. A failed command exits with status 1.} + +\begin{danger} +\textbf{Don't type a passphrase straight into a command line.} It lands in your +shell history. Read it without echo instead:\\[2pt] +\cmd{read -rs VAULT\_PASSPHRASE \&\& export VAULT\_PASSPHRASE}\\[2pt] +And remember that other programs running as you can read your environment. +For everyday use, just let vault prompt you. +\end{danger} + +\section{Exit codes and output} + +\begin{vtable}{>{\ttfamily\small}p{16mm}Y} +\thead{CODE} & \thead{MEANING} \\ \midrule +0 & It worked. \\ +1 & Something went wrong. The reason is on standard error, starting \cmd{vault:}. \\ +2 & You ran \cmd{vault} with no command at all. The usage text is on standard error. \\ +\end{vtable} + +\cmd{list} and \cmd{help} write to standard output. Progress lines like +\cmd{added \ldots}, \cmd{replaced \ldots} and every error go to standard +error. So \cmd{vault list > files.txt} gives you a clean file list. + +\begin{squirrel} +The \cmd{list} columns are fixed-width: size in 10 characters, two spaces, +the date and time in 16, two spaces, then the name. The name therefore always +starts at character 31, which is why \cmd{cut -c31-} works even for names with +spaces in. +\end{squirrel} + +\section{Recipes} + +\subsection{Nightly: stash a folder} +\begin{shell} +#!/bin/sh +# ~/bin/stash-documents: re-adds everything in ~/Documents/private +export VAULT_PASSPHRASE="$(cat ~/.config/vault-pass)" # chmod 600 this file! +vault add ~/Documents/private archive || exit 1 +\end{shell} + +Then run it from cron, say at 2am: \cmd{0 2 * * * \$HOME/bin/stash-documents}. +Re-adding replaces each file, so it always holds the latest copy. + +\begin{headsup} +Keeping the passphrase in a file trades safety for convenience. Anyone who can +read that file and the key can open the vault. Only do it on a machine you +trust completely, and never for the decoy. +\end{headsup} + +\subsection{Get everything back out} +\begin{shell} +#!/bin/sh +# restore the whole vault into ./restore, keeping its folders +vault list | cut -c31- | while IFS= read -r name; do + mkdir -p "restore/$(dirname "$name")" + vault get "$name" -o "restore/$name" +done +\end{shell} + +\subsection{PowerShell on Windows} +\begin{shell} +PS> $env:VAULT_DIR = "D:\vault" +PS> vault add .\report.docx +PS> vault list +\end{shell} + +\begin{pocket} +\begin{pocketlist} + \item \env{VAULT\_PASSPHRASE}, \env{VAULT\_KEY}, \env{VAULT\_DIR}: no prompts, no flags. + \item Exit 0 is success, 1 is an error, 2 is ``no command''. + \item \cmd{list} goes to stdout, so it pipes cleanly. Names start at column 31. + \item Keep passphrases out of your shell history. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Locking, syncing and backups} +\tagline{two terminals, two machines, and keeping copies} + +\begin{tldr} +vault locks itself: one writer at a time, and readers wait for writers. You +don't have to do anything. Syncing the folder between machines works if only +one machine changes it at a time. Back up the folder and the key, and you're +covered. +\end{tldr} + +\section{Locking is automatic} + +Commands that change the vault (\cmd{add}, \cmd{mv}, \cmd{rm}) take an +\hl{exclusive} lock. Commands that only read (\cmd{list}, \cmd{get}) take a +\hl{shared} one, so several can read at once. If a command has to wait, it +says so, then carries on by itself: + +\shot[1.2]{locking}{The second writer waits politely for the first to finish} + +The lock is taken \emph{after} you've typed your passphrase, so a colleague +who's slow at typing never blocks you. + +\begin{whyso} +\textbf{What if vault crashes while holding the lock?} On Linux, macOS, the +BSDs and Windows, the operating system drops the lock the moment the process +dies. Nothing to clean up. On Plan~9 and WebAssembly, which have no file +locks, vault uses a \file{.lock} file instead. If one's ever left behind, +vault tells you after a minute exactly which file to delete. +\end{whyso} + +\section{Syncing between machines} + +A vault is just a folder, so Syncthing, Dropbox, iCloud Drive, OneDrive, +\cmd{rsync} or a USB stick will all carry it between computers. Copy your key +file to each machine too, over something private. + +\begin{headsup} +\textbf{Locks don't travel through sync services.} Two machines writing at +the same time won't wait for each other. The rule: \hl{one machine writes at a +time, and let the sync finish before you write on the other one.} +\end{headsup} + +If you do slip up, it's rarely a disaster: + +\begin{itemize} + \item Sync ``conflicted copy'' files have names vault doesn't recognise, so + it simply ignores them. + \item If both machines added the same name, \cmd{list} shows the newest, + and the next \cmd{add} or \cmd{rm} of that name tidies up the other. +\end{itemize} + +Network file systems are fine too. NFS honours vault's locks on Linux and the +BSDs, and Windows file shares honour them from Windows. + +\section{Backups} + +Two things to back up: the \hl{vault folder} and the \hl{key file}. Ideally +in different places. Any backup tool works, because it's all ordinary files. + +\begin{quickwin} +vault never edits a file in place. Every entry is written once and later +deleted, never changed. That makes incremental backups (\cmd{rsync}, +restic, borg, Time Machine) fast and tidy: each run only copies what's new. +\end{quickwin} + +\begin{shell} +$ rsync -a --delete ~/.vault/store/ /media/backup/vault-store/ +\end{shell} + +\begin{headsup} +A backup of the vault is only as good as your ability to open it. Test it: +point \cmd{--vault} at the backup copy and run \cmd{vault list} now and then. +\end{headsup} + +\begin{pocket} +\begin{pocketlist} + \item Locking is automatic. Writers take turns; readers share. + \item Sync across machines freely, but write on one machine at a time. + \item Back up the folder and the key, separately. Test the backup. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Under the hood} +\tagline{the cryptography, the file layout, and exactly what a thief learns} + +\begin{tldr} +Each file gets its own random 256-bit key, wrapped with a hybrid of FrodoKEM +and ML-KEM, and is encrypted in authenticated 64\,KiB chunks. Catalogue +entries are padded to one fixed size, file contents to power-of-two buckets, +and every timestamp on disk is set to 1970. An attacker learns roughly how +much you store. They can delete or roll back entries, but not read or forge them. +\end{tldr} + +\section{The cryptography} + +\begin{vtable}{>{\semibold}p{38mm}Y} +\thead{JOB} & \thead{HOW} \\ \midrule +Protecting your key & Argon2id (3 passes, 64\,MiB, 4 lanes) turns the passphrase into a key that seals your identity with XChaCha20-Poly1305. \\ +Your identity & A 32-byte seed from which the FrodoKEM-640 and ML-KEM-768 key pairs are derived. \\ +Per-file key & 256 random bits, fresh for every single entry. \\ +Wrapping that key & FrodoKEM-640 \emph{and} ML-KEM-768 encapsulations, combined with HKDF-SHA-256 into one key-encryption key, then ChaCha20-Poly1305. \\ +Header integrity & HMAC-SHA-256 under the per-file key. \\ +The contents & ChaCha20-Poly1305 in a STREAM construction: 64\,KiB chunks, each authenticated, with the last one marked so truncation is caught. \\ +\end{vtable} + +\begin{whyso} +\textbf{Why two post-quantum algorithms?} ML-KEM is the new standard, +efficient and heavily analysed. FrodoKEM is slower and bulkier, but rests on +older, more conservative maths. Combining them means a breakthrough against +either one alone isn't enough. +\end{whyso} + +\section{What's in the folder} + +\begin{center} +\begin{tikzpicture}[ + dir/.style={font=\small\ttfamily\color{vskydeep},anchor=west}, + f/.style={font=\small\ttfamily\color{vtext},anchor=west}, + note/.style={font=\footnotesize\color{vslate},anchor=west}, + tree/.style={draw=vslate!45,line width=0.8pt}] + \node[dir] (root) at (0,0) {\tld/.vault/store/}; + \node[f] (m) at (0.6,-0.7) {.vault}; \node[note] at (4.2,-0.7) {marker: ``VAULT 1''}; + \node[f] (l) at (0.6,-1.3) {.lock}; \node[note] at (4.2,-1.3) {lock file, always empty}; + \node[dir] (r) at (0.6,-1.9) {records/}; \node[note] at (4.2,-1.9) {one encrypted catalogue entry per file}; + \node[f] (r1) at (1.2,-2.5) {0f1170a6\ldots}; \node[note] at (4.2,-2.5) {every one exactly 18,704 bytes}; + \node[dir] (o) at (0.6,-3.1) {objects/}; \node[note] at (4.2,-3.1) {one encrypted file body per file}; + \node[f] (o1) at (1.2,-3.7) {3e78275e\ldots}; \node[note] at (4.2,-3.7) {64\,KiB, 128\,KiB, 256\,KiB\ldots\ never in between}; + \draw[tree] (0.3,-0.25) |- (m.west); \draw[tree] (0.3,-0.7) |- (l.west); \draw[tree] (0.3,-1.3) |- (r.west); + \draw[tree] (0.3,-1.9) |- (o.west); + \draw[tree] (0.9,-2.15) |- (r1.west); \draw[tree] (0.9,-3.35) |- (o1.west); +\end{tikzpicture} +\end{center} + +A \hl{record} holds one file's name, size, permissions, dates and the ID of +its object. It's padded to 4096 bytes before encryption, so every record is +the same size whatever the name. An \hl{object} holds the file's contents, +prefixed with its own ID and padded with random bytes up to the next power +of two, at least 64\,KiB. + +\shot{on-disk}{Random names, identical sizes, and all ``modified'' at the dawn of Unix time} + +\section{What an attacker learns} + +\begin{vtable}{Y>{\centering\arraybackslash}p{22mm}} +\thead{SOMEONE WITH A COPY OF YOUR VAULT FOLDER\ldots} & \thead{CAN THEY?} \\ \midrule +Read any file's contents & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Learn file names or folder names & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Learn exact file sizes & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Learn when a file was created or changed & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Tell real entries from decoy or filler & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Alter a file without it being detected & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Swap one file's contents for another's & \textcolor{vgreendeep}{\faIcon{times}\ no} \\ +Count entries, and see rough size buckets & \textcolor{vamberdeep}{\faIcon{check}\ yes} \\ +See that the folder changed between two copies & \textcolor{vamberdeep}{\faIcon{check}\ yes} \\ +Delete entries & \textcolor{vcoraldeep}{\faIcon{check}\ yes} \\ +Put back an older copy of an entry (roll back) & \textcolor{vcoraldeep}{\faIcon{check}\ yes} \\ +\end{vtable} + +\begin{headsup} +\textbf{Deletion and rollback aren't detectable.} vault can prove a file is +exactly what you stored, but not that it's the \emph{latest} thing you stored. +If the storage isn't under your control, keep an independent backup. +\end{headsup} + +\section{What happens during \texttt{add}} + +\begin{steps} + \item Read your file and encrypt its contents, plus random padding, to a + hidden temporary file in \file{objects/}. + \item Give it its random name. It's now safely stored, but nothing refers to it. + \item Encrypt a record pointing at it, the same way, into \file{records/}. + \item Only then, delete any older version of the same name. +\end{steps} + +Interrupt it anywhere and the worst case is an orphaned encrypted object that +nothing points to. You never end up with a record pointing at nothing, or +without either version. + +\begin{squirrel} +vault's crypto core comes from \emph{Fury}, a larger post-quantum encryption +toolkit. Peek inside any entry and the first line still reads +\cmd{fury-encryption.org/v1}. That's the format header, kept on purpose: +changing working, well-tested cryptographic formats for aesthetic reasons +is how bugs get in. +\end{squirrel} + +\begin{pocket} +\begin{pocketlist} + \item Hybrid post-quantum key wrapping, per-file keys, authenticated streaming encryption. + \item Fixed-size records, power-of-two objects, 1970 timestamps: metadata stays private. + \item Tampering is caught. Deletion and rollback are not, so keep backups. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{When things go sideways} +\tagline{every message vault can show you, and what to do about it} + +\begin{tldr} +Almost every error is one of four things: wrong passphrase, wrong \cmd{--key} +or \cmd{--vault}, a name typo, or a file already in the way. Check the table. +Your data is still fine. +\end{tldr} + +\shot{errors}{The usual suspects} + +\section{The big table} + +\begin{vtable}{>{\ttfamily\footnotesize}p{58mm}Y} +\thead{MESSAGE} & \thead{WHAT IT MEANS, AND THE FIX} \\ \midrule +incorrect passphrase & Typo, Caps Lock, or the wrong key file. Check \cmd{--key} and \env{VAULT\_KEY}. \\ +not a vault key file & \cmd{--key} points at something that isn't a vault key, or the key file is damaged. Use your backup. \\ +open \ldots/key: no such file or directory & No key there yet. Run \cmd{vault genkey}, or point \env{VAULT\_KEY} at yours. \\ +\ldots\ is not a vault; run vault init & Wrong \cmd{--vault} or \env{VAULT\_DIR}, or you haven't run \cmd{init} yet. \\ +\ldots\ is already a vault & You ran \cmd{init} on an existing vault. Nothing was changed. \\ +NAME: not in the vault & Check the exact name with \cmd{vault list}: it's case-sensitive and includes the folder. Or you typed the other passphrase. \\ +\ldots\ already exists & \cmd{get}: the output file exists, so use \cmd{-o}. \cmd{mv}: the new name is taken. \cmd{genkey}: you already have a key. \\ +invalid vault path "\ldots" & Names can't start with \cmd{/} or \cmd{../}, end in \cmd{/}, or contain backslashes or control characters. \\ +\ldots: not a regular file of at most 1\,TiB & You tried to add a device, a socket or a gigantic file. \\ +\ldots\ changed while being added & The file was being written while vault read it. Try again once it's finished. \\ +skipping \ldots: not a regular file & Adding a folder: a symlink or special file was skipped. Everything else went in. \\ +passphrases do not match & The two entries differed. Nothing was created; run it again. \\ +passphrase must not be empty & Blank passphrases aren't allowed, for good reason. \\ +decoy passphrase must differ from the passphrase & The decoy can't be the same as the real one. \\ +VAULT\_PASSPHRASE is set but empty & Unset it, or give it a value. \\ +could not read passphrase (set VAULT\_PASSPHRASE\ldots) & No terminal to ask on, typically cron or CI. Set the variable. \\ +waiting for another vault process to finish & Not an error. Another command is writing; this one continues when it's done. \\ +vault still locked after a minute\ldots & Plan~9 and WebAssembly only. If nothing else is running, delete the file it names. \\ +skipping unreadable record \ldots & A catalogue entry is damaged. Everything else still works; restore \file{records/} from backup to recover that file. \\ +object does not match its catalogue record & The encrypted contents were swapped or damaged. Nothing was written. Restore \file{objects/} from backup. \\ +object size does not match its catalogue record & Same as above: damage or tampering, caught before any output appeared. \\ +cannot find home directory; set VAULT\_KEY & Unusual environments only. Set \env{VAULT\_KEY} and \env{VAULT\_DIR}. \\ +unknown command / wrong number of arguments & A typo in the command itself. The usage text follows. \\ +\end{vtable} + +\section{``My files have vanished!''} + +Deep breath. Nine times out of ten, one of these: + +\begin{checks} + \item \textbf{You typed the other passphrase.} The decoy and real sides are + separate worlds. Try the other one. + \item \textbf{You're looking at a different vault.} Check \cmd{--vault} and + \env{VAULT\_DIR}. Is this the vault you think it is? + \item \textbf{You're looking in a folder.} \cmd{vault list letters} only + shows what's under \file{letters/}. +\end{checks} + +\section{Interrupted in the middle of something?} + +Power cut, closed laptop, \keycap{Ctrl}+\keycap{C}. It's fine. vault writes +new data first and removes old data last, so an interruption leaves the vault +consistent. At worst there's an orphaned encrypted object, or a temporary +file starting \file{.pending-}. Both are harmless and both are safe to delete. + +\begin{danger} +\textbf{The one thing that can't be fixed:} a lost key file or a forgotten +passphrase. Nothing in this chapter helps with that. Back up your key. +\end{danger} + +\begin{pocket} +\begin{pocketlist} + \item Most errors mean a wrong passphrase, key, vault or name. + \item ``Missing'' files usually mean the other passphrase or another vault. + \item Interruptions are safe. Lost keys are not. +\end{pocketlist} +\end{pocket} + +% ====================================================================== +\chapter{Cheat sheet} +\tagline{the whole tool at a glance. print it, pin it up} + +\begin{tcbraster}[raster columns=2,raster equal height=rows,raster column skip=8pt,raster row skip=8pt, + enhanced,colback=vmist,colframe=vline,boxrule=0.4pt,arc=4pt,left=9pt,right=9pt,top=7pt,bottom=7pt] +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault genkey}\\[2pt]\small Make a key. Asks for a passphrase and an optional decoy.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault init}\\[2pt]\small Make an empty vault, with filler.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault add FILE|DIR [FOLDER]}\\[2pt]\small Encrypt in, keeping the name. Replaces an existing name.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault list [FOLDER]}\\[2pt]\small Size, modified time and name, sorted.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault get NAME [-o OUT]}\\[2pt]\small Decrypt out. Never overwrites. Verifies first.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault mv OLD NEW}\\[2pt]\small Rename, using the full new name. Instant.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault rm NAME}\\[2pt]\small Delete one file. No undo.\end{tcolorbox} +\begin{tcolorbox}{\ttfamily\color{vtealdeep}\bfseries vault help}\\[2pt]\small Show the command summary.\end{tcolorbox} +\end{tcbraster} + +\section*{Options and variables} +\begin{vtable}{>{\ttfamily\small}p{40mm}>{\leavevmode\ttfamily\small\color{vvioletdeep}}p{48mm}Y} +\thead{FLAG} & \thead{VARIABLE} & \thead{DEFAULT} \\ \midrule +--key PATH & VAULT\_KEY & \file{\tld/.vault/key} \\ +--vault DIR & VAULT\_DIR & \file{\tld/.vault/store} \\ +\textrm{(prompt)} & VAULT\_PASSPHRASE & asked on the terminal \\ +\textrm{(genkey prompt)} & VAULT\_DECOY\_PASSPHRASE & asked; empty means none \\ +-o OUT & \textrm{---} & the file's own name, here \\ +\end{vtable} + +\section*{Numbers worth knowing} +\begin{vtable}{Y>{\raggedleft\arraybackslash}p{40mm}} +\thead{THING} & \thead{VALUE} \\ \midrule +Largest file & 1\,TiB \\ +Longest stored name & 1024 bytes \\ +Smallest object on disk & 64\,KiB \\ +Filler entries in a new vault & 8 to 32 \\ +Key file size & 319 bytes \\ +Exit codes & 0 ok, 1 error, 2 no command \\ +\end{vtable} + +% ====================================================================== +\appendix +\vappendixtrue + +\chapter{Glossary} + +\begin{description}[style=nextline,leftmargin=0pt,font=\displaybold\color{vink},itemsep=4pt] + \item[Argon2id] A deliberately slow, memory-hard function that turns your passphrase into a key, making guessing expensive. + \item[Catalogue] The encrypted list of what's in your vault, stored as one record per file. + \item[Decoy passphrase] A second passphrase that opens a separate, harmless set of files. + \item[Entry] One record plus its object. Real, decoy and filler entries all look identical on disk. + \item[Filler] Entries sealed to a key that was immediately destroyed. Nobody can open them. They make the decoy plausible. + \item[FrodoKEM, ML-KEM] Post-quantum key-encapsulation algorithms. vault uses both at once. + \item[Key file] The small file holding your identity, encrypted under your passphrase (and decoy). + \item[Object] The encrypted contents of one file, padded to a power-of-two size. + \item[Post-quantum] Designed to stay secure against attackers with large quantum computers. + \item[Record] The encrypted catalogue entry for one file: its name, size, dates and object ID. + \item[Vault] The folder where your encrypted files live. +\end{description} + +\chapter{About this guide} + +This guide lives in \file{docs/guide} and builds itself. + +\begin{shell} +$ ./build-pdf.sh # makes vault-guide.pdf (downloads Tectonic if needed) +$ ./shots.py # re-shoots every screenshot from the real vault binary +\end{shell} + +Every screenshot is real output from the \cmd{vault} program built from the +same source code, run in a throwaway home folder. The only touch-ups: the home +folder is shown as \file{/home/sam}, some lines are highlighted in amber +so they're easier to spot, and the two ``waiting for your passphrase'' shots +are frozen at that moment. + +% ====================================================================== +\backmatter +\clearpage +\thispagestyle{empty} +\begin{tikzpicture}[remember picture,overlay] + \shade[top color=vink,bottom color=vnavy] (current page.north west) rectangle (current page.south east); + \vaultdoor{($(current page.center)+(0,30mm)$)}{38} + \node[font=\displayblack\fontsize{40}{40}\selectfont,text=white] at ([yshift=-38mm]current page.center) {That's it.}; + \node[font=\lightfont\fontsize{15}{20}\selectfont,text=white!80!vteal,align=center] at ([yshift=-56mm]current page.center) + {Go encrypt something.\\ (And back up your key.)}; +\end{tikzpicture} + +\end{document} diff --git a/docs/guide/vaultguide.sty b/docs/guide/vaultguide.sty new file mode 100644 index 0000000..f2fa1c4 --- /dev/null +++ b/docs/guide/vaultguide.sty @@ -0,0 +1,293 @@ +% vaultguide.sty — the look of the vault field guide. +% Compiled with Tectonic (XeTeX). All fonts come from the TeX bundle. +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{vaultguide} + +\PassOptionsToPackage{table}{xcolor} +\RequirePackage[a4paper,top=27mm,bottom=26mm,left=26mm,right=26mm, + headheight=16pt,headsep=9mm,footskip=13mm]{geometry} +\RequirePackage{fontspec} +\RequirePackage{xcolor} +\RequirePackage{tikz} +\usetikzlibrary{calc,positioning,arrows.meta,shadows.blur,backgrounds,fit,shapes.misc} +\RequirePackage[most]{tcolorbox} +\RequirePackage{fontawesome5} +\RequirePackage{graphicx} +\RequirePackage{titlesec} +\RequirePackage{titletoc} +\RequirePackage{fancyhdr} +\RequirePackage{enumitem} +\RequirePackage{booktabs,tabularx,array} +\RequirePackage{listings} +\RequirePackage{xstring} +\RequirePackage{microtype} +\RequirePackage[hidelinks]{hyperref} +\RequirePackage{bookmark} + +% ---------------------------------------------------------------- colours +\definecolor{vnavy}{HTML}{0B1020} +\definecolor{vink}{HTML}{1A2244} +\definecolor{vteal}{HTML}{19C3A6} +\definecolor{vtealdeep}{HTML}{0B8C77} +\definecolor{vamber}{HTML}{FFB020} +\definecolor{vamberdeep}{HTML}{B87400} +\definecolor{vcoral}{HTML}{FF5D73} +\definecolor{vcoraldeep}{HTML}{C8243E} +\definecolor{vviolet}{HTML}{7C5CFF} +\definecolor{vvioletdeep}{HTML}{5337D6} +\definecolor{vsky}{HTML}{4F8CFF} +\definecolor{vskydeep}{HTML}{2A62D6} +\definecolor{vgreen}{HTML}{2BB673} +\definecolor{vgreendeep}{HTML}{178453} +\definecolor{vmist}{HTML}{F2F5FB} +\definecolor{vline}{HTML}{DCE3F0} +\definecolor{vslate}{HTML}{5B6788} +\definecolor{vtext}{HTML}{1E2433} + +% ---------------------------------------------------------------- fonts +\defaultfontfeatures{Ligatures=TeX} +\setmainfont{Inter-Regular.otf}[ + BoldFont=Inter-Bold.otf, ItalicFont=Inter-Italic.otf, + BoldItalicFont=Inter-BoldItalic.otf] +\setmonofont{FiraMono-Regular.otf}[BoldFont=FiraMono-Bold.otf, Scale=0.92] +\newfontfamily\semibold{Inter-SemiBold.otf} +\newfontfamily\lightfont{Inter-Light.otf}[ItalicFont=Inter-Italic.otf] +\newfontfamily\display{Montserrat-ExtraBold.otf} +\newfontfamily\displayblack{Montserrat-Black.otf} +\newfontfamily\displaybold{Montserrat-Bold.otf} +\newfontfamily\displaysemi{Montserrat-SemiBold.otf} +\newfontfamily\displaylight{Montserrat-Light.otf} +\newfontfamily\displaythin{Montserrat-Thin.otf} +\newfontfamily\labelfont{Montserrat-Bold.otf}[LetterSpace=14] +\newfontfamily\labelfontwide{Montserrat-SemiBold.otf}[LetterSpace=30] + +\color{vtext} +\linespread{1.14} +\setlength\parindent{0pt} +\setlength\parskip{0.62em plus 0.15em minus 0.05em} +\setlength\emergencystretch{2em} +\raggedbottom + +% ---------------------------------------------------------------- small helpers +\newcommand\cmd[1]{\leavevmode{\ttfamily\color{vtealdeep}#1}} +\newcommand\tld{\textasciitilde} +\newcommand\env[1]{\leavevmode{\ttfamily\color{vvioletdeep}#1}} +\newcommand\file[1]{\leavevmode{\ttfamily\color{vskydeep}#1}} +\newcommand\hl[1]{\leavevmode{\semibold\color{vink}#1}} +\newcommand\keycap[1]{\tikz[baseline=(k.base)]\node[draw=vline,fill=white,rounded corners=2pt, + inner xsep=4pt,inner ysep=2pt,font=\footnotesize\semibold, + blur shadow={shadow xshift=0pt,shadow yshift=-0.6pt,shadow blur steps=3,shadow opacity=25}](k){#1};} +\newcommand\pill[2][vteal]{\tikz[baseline=(p.base)]\node[fill=#1!14,text=#1!70!black,rounded corners=5pt, + inner xsep=5pt,inner ysep=2pt,font=\scriptsize\labelfont](p){#2};} +\newcommand\stepdot[1]{\tikz[baseline=(c.base)]\node[circle,fill=vteal,text=white, + font=\scriptsize\displaybold,inner sep=0pt,minimum size=15pt](c){#1};} + +% A screenshot. Optional width fraction, file name in img/, caption. +\newcommand\shot[3][1.1]{% + \par\smallskip + \begin{center} + \makebox[\linewidth]{\includegraphics[width=#1\linewidth]{img/#2}}\par\vspace{-6pt} + {\footnotesize\color{vslate}\faIcon[regular]{camera}\enspace #3} + \end{center}\par\smallskip} + +% ---------------------------------------------------------------- lists +\setlist{itemsep=2pt,topsep=4pt,parsep=1pt} +\setlist[itemize,1]{label={\color{vteal}\footnotesize\faIcon{chevron-right}},leftmargin=1.6em} +\setlist[itemize,2]{label={\color{vslate}\scriptsize\faIcon[regular]{circle}},leftmargin=1.4em} +\setlist[enumerate,1]{label={\color{vtealdeep}\displaybold\arabic*.},leftmargin=1.8em} +\newlist{steps}{enumerate}{1} +\setlist[steps]{label=\protect\stepdot{\arabic*},leftmargin=2.3em,itemsep=5pt,labelsep=0.6em} +\newlist{checks}{itemize}{1} +\setlist[checks]{label={\color{vtealdeep}\faIcon[regular]{check-square}},leftmargin=1.9em,itemsep=3pt} +\newlist{nopes}{itemize}{1} +\setlist[nopes]{label={\color{vcoraldeep}\faIcon[regular]{times-circle}},leftmargin=1.9em,itemsep=3pt} + +% ---------------------------------------------------------------- callout boxes +\tcbset{ + vbox/.style 2 args={enhanced,breakable,colback=#1!7!white,colframe=#1,boxrule=0pt, + leftrule=3.2pt,arc=3pt,outer arc=3pt,left=11pt,right=11pt,top=8pt,bottom=8pt, + before skip=10pt,after skip=12pt, + before upper={{\labelfont\scriptsize\color{#1!75!black}#2}\par\vspace{3pt}}}, +} +\newtcolorbox{tldr}{vbox={vteal}{\faIcon{bolt}\enspace TL;DR},colback=vteal!9!white,fontupper=\semibold} +\newtcolorbox{quickwin}{vbox={vgreen}{\faIcon{rocket}\enspace QUICK WIN}} +\newtcolorbox{headsup}{vbox={vamber}{\faIcon{exclamation-triangle}\enspace HEADS UP}} +\newtcolorbox{danger}{vbox={vcoral}{\faIcon{skull-crossbones}\enspace HERE BE DRAGONS}} +\newtcolorbox{squirrel}{vbox={vviolet}{\faIcon{paw}\enspace SQUIRREL! \normalfont\itshape\color{vslate}(a tangent. skip it if you like)}} +\newtcolorbox{whyso}{vbox={vsky}{\faIcon[regular]{lightbulb}\enspace WHY THOUGH?}} +\newtcolorbox{pocket}{enhanced,breakable,colback=vnavy,colframe=vnavy,arc=5pt,boxrule=0pt, + left=14pt,right=14pt,top=11pt,bottom=11pt,before skip=16pt,after skip=10pt, + colupper=white, + before upper={{\labelfont\scriptsize\color{vteal}\faIcon{brain}\enspace POCKET THIS}\par\vspace{4pt}}, + overlay={\node[anchor=north east,font=\fontsize{46}{46}\selectfont,text=vteal,opacity=0.13] + at ([xshift=-8pt,yshift=-4pt]frame.north east) {\faIcon{brain}};}} +\newenvironment{pocketlist}{\begin{itemize}[label={\color{vteal}\faIcon{check}},leftmargin=1.7em,itemsep=3pt]}{\end{itemize}} + +% ---------------------------------------------------------------- shell listings +\lstdefinestyle{shell}{ + basicstyle=\ttfamily\small\color{white!90!vmist}, + columns=fullflexible,keepspaces=true,showstringspaces=false, + breaklines=true,breakatwhitespace=true, + postbreak=\mbox{\textcolor{vslate}{$\hookrightarrow$}\space}, + morecomment=[l]{\#\ },commentstyle=\color{vslate!55!white}, + literate={\$\ }{{\textcolor{vteal}{\$}\ }}2 {~}{{\textasciitilde}}1, + upquote=true} +\newtcblisting{shell}{enhanced,breakable,listing only,listing options={style=shell}, + colback=vnavy,colframe=vnavy,boxrule=0pt,arc=5pt,left=12pt,right=12pt,top=13pt,bottom=7pt, + before skip=8pt,after skip=10pt, + overlay={\foreach \c [count=\i] in {vcoral,vamber,vgreen} + {\fill[\c] ([xshift=6pt+\i*8pt,yshift=-7pt]frame.north west) circle (2.1pt);}}} +\newtcblisting{plain}{enhanced,breakable,listing only, + listing options={basicstyle=\ttfamily\small\color{vtext},columns=fullflexible,keepspaces, + upquote=true,literate={~}{{\textasciitilde}}1}, + colback=vmist,colframe=vline,boxrule=0.4pt,arc=4pt,left=10pt,right=10pt,top=6pt,bottom=6pt} + +% ---------------------------------------------------------------- tables +\newcolumntype{Y}{>{\raggedright\arraybackslash}X} +\newcommand\thead[1]{\leavevmode{\labelfont\scriptsize\color{vslate}#1}} +\newenvironment{vtable}[1] + {\par\smallskip\rowcolors{2}{vmist}{white}\renewcommand\arraystretch{1.45}% + \small\tabularx{\linewidth}{#1}\toprule} + {\bottomrule\endtabularx\par\smallskip} + +% ---------------------------------------------------------------- headers & footers +\newcommand\pagepill{\tikz[baseline=(n.base)]\node[fill=vnavy,text=white,rounded corners=7pt, + inner xsep=8pt,inner ysep=3.5pt,font=\footnotesize\displaybold](n){\thepage};} +\pagestyle{fancy} +\fancyhf{} +\renewcommand\headrulewidth{0pt} +\fancyhead[L]{\footnotesize\displaysemi\color{vslate}\leftmark} +\fancyhead[R]{\footnotesize\color{vslate}{\displaybold\color{vteal}vault}\enspace field guide} +\fancyfoot[C]{\pagepill} +\fancypagestyle{plain}{\fancyhf{}\fancyfoot[C]{\pagepill}} +\renewcommand\chaptermark[1]{\markboth{#1}{}} +\renewcommand\sectionmark[1]{} + +% ---------------------------------------------------------------- chapter banners +\newif\ifvappendix +\newcommand\vaultchaptercount{11} +\newcommand\vbanner[3]{% #1 label, #2 big mark, #3 title + \begin{tikzpicture}[remember picture,overlay] + \shade[left color=vnavy,right color=vink] + (current page.north west) rectangle ([yshift=-72mm]current page.north east); + \begin{scope} + \clip (current page.north west) rectangle ([yshift=-72mm]current page.north east); + \foreach \r in {14,24,34,44,54,64} + \draw[vteal,opacity=0.13,line width=0.7pt] + ([xshift=-34mm,yshift=-38mm]current page.north east) circle (\r mm); + \foreach \a in {0,6,...,354} + \draw[vteal,opacity=0.22,line width=0.5pt] + ([xshift=-34mm,yshift=-38mm]current page.north east) ++(\a:44mm) -- ++(\a:3mm); + \node[anchor=center,font=\displayblack\fontsize{96}{96}\selectfont,text=vteal] + at ([xshift=-34mm,yshift=-38mm]current page.north east) {#2}; + \end{scope} + \fill[vamber] ([yshift=-72mm]current page.north west) rectangle + ([yshift=-73.4mm,xshift=62mm]current page.north west); + \node[anchor=south west,font=\labelfont\scriptsize,text=vteal] + at ([xshift=26mm,yshift=-27mm]current page.north west) {#1}; + \node[anchor=north west,text width=118mm,align=left,text=white, + font={\display\fontsize{27}{31}\selectfont\hyphenpenalty=10000\exhyphenpenalty=10000}] + at ([xshift=25.4mm,yshift=-30mm]current page.north west) {#3}; + \ifvappendix\else\ifnum\value{chapter}>0 + \foreach \i in {1,...,\vaultchaptercount}{ + \ifnum\i<\numexpr\value{chapter}+1\relax + \fill[vteal] ([xshift=26mm+\i*5.2mm-5.2mm,yshift=-63mm]current page.north west) circle (1.5pt); + \else + \draw[vteal!60,line width=0.5pt] ([xshift=26mm+\i*5.2mm-5.2mm,yshift=-63mm]current page.north west) circle (1.5pt); + \fi} + \node[anchor=west,font=\scriptsize\color{white!60!vslate}] + at ([xshift=26mm+\vaultchaptercount*5.2mm,yshift=-63mm]current page.north west) + {you are here}; + \fi\fi + \end{tikzpicture}% + \par\vspace*{40mm}} +\newcommand\vchapterhead[1]{\ifvappendix\vbanner{APPENDIX \thechapter}{\thechapter}{#1}\else + \vbanner{CHAPTER \thechapter\ \ /\ \ \vaultchaptercount}{\thechapter}{#1}\fi} +\titleformat{\chapter}[block]{}{}{0pt}{\vchapterhead} +\titleformat{name=\chapter,numberless}[block]{}{}{0pt}{\vbanner{THE VAULT FIELD GUIDE}{\faIcon{key}}} +\titlespacing*{\chapter}{0pt}{-14mm}{6mm} + +\titleformat{\section}[hang]{\display\fontsize{16.5}{20}\selectfont\color{vink}} + {\llap{\color{vteal}\thesection\hspace{0.65em}}}{0pt}{} +\titlespacing*{\section}{0pt}{18pt plus 4pt minus 2pt}{6pt} +\newcommand\vsubhead[1]{\llap{\color{vteal}\scriptsize\faIcon{square-full}\hspace{0.6em}}#1} +\titleformat{\subsection}[block]{\displaybold\fontsize{12}{15}\selectfont\color{vink}} + {}{0pt}{\vsubhead} +\titlespacing*{\subsection}{0pt}{12pt plus 3pt}{3pt} +\setcounter{secnumdepth}{1} +\setcounter{tocdepth}{1} + +% ---------------------------------------------------------------- contents +\titlecontents{chapter}[0pt]{\addvspace{9pt}\display\fontsize{12.5}{16}\selectfont\color{vink}} + {\makebox[11mm][l]{\color{vteal}\thecontentslabel}}{} + {\hfill\color{vtealdeep}\contentspage} +\titlecontents{section}[11mm]{\addvspace{1pt}\small\color{vtext}} + {\makebox[10mm][l]{\color{vslate}\thecontentslabel}}{} + {\color{vline}\titlerule*[0.55pc]{.}\color{vslate}\contentspage} +\newcommand\tagline[1]{\addtocontents{toc}{\protect\vspace{-1pt}\protect\hspace*{11mm}% + {\protect\footnotesize\protect\itshape\protect\color{vslate}#1}\protect\par}} + +% ---------------------------------------------------------------- cover +\newcommand\vaultdoor[2]{% #1 centre coordinate, #2 radius in mm + \begin{scope}[shift={#1}] + \foreach \g/\o in {1.42/0.035,1.3/0.05,1.2/0.07,1.12/0.09} + \fill[vteal,opacity=\o] (0,0) circle (#2*\g mm); + \fill[vink] (0,0) circle (#2 mm); + \draw[vteal,line width=1.6pt] (0,0) circle (#2 mm); + \draw[vteal!55,line width=0.5pt] (0,0) circle (#2*0.93 mm); + \foreach \a in {0,3,...,357} + \draw[vteal!70,line width=0.45pt] (\a:#2*0.93 mm) -- (\a:#2*0.885 mm); + \foreach \a in {0,30,...,330} + \draw[vteal,line width=1.1pt] (\a:#2*0.93 mm) -- (\a:#2*0.85 mm); + \fill[vnavy] (0,0) circle (#2*0.76 mm); + \draw[vteal!40,line width=0.6pt] (0,0) circle (#2*0.76 mm); + \foreach \a in {22.5,67.5,...,337.5} + {\fill[vink] (\a:#2*0.66 mm) circle (#2*0.055 mm); + \draw[vteal!80,line width=0.6pt] (\a:#2*0.66 mm) circle (#2*0.055 mm);} + \draw[vamber,line width=2.2pt,line cap=round] (0,0) circle (#2*0.34 mm); + \foreach \a in {90,210,330} + {\draw[white,line width=3.6pt,line cap=round] (\a:#2*0.1 mm) -- (\a:#2*0.5 mm); + \fill[white] (\a:#2*0.5 mm) circle (#2*0.045 mm);} + \fill[white] (0,0) circle (#2*0.12 mm); + \fill[vamber] (0,0) circle (#2*0.055 mm); + \end{scope}} + +\newcommand\makecover{% + \thispagestyle{empty} + \begin{tikzpicture}[remember picture,overlay] + \shade[top color=vink,bottom color=vnavy] + (current page.north west) rectangle (current page.south east); + % ciphertext rain + \pgfmathsetseed{1789} + \begin{scope} + \clip (current page.north west) rectangle (current page.south east); + \foreach \y in {0,...,58}{ + \pgfmathrandominteger{\off}{0}{60} + \node[anchor=west,font=\ttfamily\fontsize{6.5}{6.5}\selectfont,text=vteal,opacity=0.075] + at ([xshift=-\off mm,yshift=-\y*5.1mm-3mm]current page.north west) + {9f3a c01d e55b 7a90 b4f2 0e6c d318 a7e4 52cb 11f0 8d6e 4ab9 e203 7c5f 96d1 3be8 f40a 6d27 c9b3 0a5e 71fd 2c84 be16 5903 d7aa 18e9 f36b 4c20 a95d};} + \end{scope} + \vaultdoor{($(current page.center)+(38mm,42mm)$)}{62} + % title block + \node[anchor=south west,font=\labelfontwide\small,text=vteal] + at ([xshift=24mm,yshift=112mm]current page.south west) {THE FIELD GUIDE}; + \node[anchor=north west,font=\displayblack\fontsize{104}{104}\selectfont,text=white] + at ([xshift=21mm,yshift=110mm]current page.south west) {vault}; + \fill[vamber] ([xshift=25mm,yshift=72mm]current page.south west) rectangle ++(34mm,1.6mm); + \node[anchor=north west,text width=130mm,align=left, + font=\lightfont\fontsize{17}{23}\selectfont,text=white!85!vteal] + at ([xshift=24mm,yshift=66mm]current page.south west) + {Post-quantum file encryption.\\ With a secret trapdoor.}; + \node[anchor=south west] at ([xshift=24mm,yshift=26mm]current page.south west) + {\coverchip{\faIcon{shield-alt}}{FrodoKEM + ML-KEM}\hspace{2.5mm}% + \coverchip{\faIcon{user-secret}}{decoy passphrase}\hspace{2.5mm}% + \coverchip{\faIcon{microchip}}{one binary, 43 platforms}}; + \node[anchor=south east,font=\scriptsize\labelfont,text=white!55!vslate] + at ([xshift=-22mm,yshift=14mm]current page.south east) {VERSION \vaultversion\ \ \textbullet\ \ \vaultdate}; + \node[anchor=south west,font=\footnotesize\itshape,text=white!55!vslate] + at ([xshift=24mm,yshift=14mm]current page.south west) {for humans who skim}; + \end{tikzpicture}} +\newcommand\coverchip[2]{\tikz[baseline=(c.base)]\node[draw=vteal!70,line width=0.6pt,rounded corners=8pt, + text=white,font=\footnotesize\semibold,inner xsep=7pt,inner ysep=4pt](c){{\color{vteal}#1}\enspace #2};} + +\endinput