Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ff8546fd | ||
|
|
0b3dcf08f2 | ||
|
|
10f2a88493 |
@@ -30,6 +30,12 @@ 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/build-pdf.sh`, which fetches Tectonic if it isn't installed.
|
||||||
`docs/guide/shots.py` regenerates its screenshots from the real binary.
|
`docs/guide/shots.py` regenerates its screenshots from the real binary.
|
||||||
|
|
||||||
|
## Man pages
|
||||||
|
|
||||||
|
`docs/*.1` holds `vault(1)` and one page per command (`vault-add(1)`,
|
||||||
|
`vault-mirror(1)` and so on). Install them with
|
||||||
|
`cp docs/*.1 ~/.local/share/man/man1/`.
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -39,6 +45,9 @@ vault init --filler 2G # the same, with at least 2 GiB of filler
|
|||||||
vault add report.pdf # stored as report.pdf
|
vault add report.pdf # stored as report.pdf
|
||||||
vault add report.pdf work/2026 # stored as work/2026/report.pdf
|
vault add report.pdf work/2026 # stored as work/2026/report.pdf
|
||||||
vault add ./photos # every regular file under photos/
|
vault add ./photos # every regular file under photos/
|
||||||
|
vault add https://example.com/a.pdf # download and store a URL
|
||||||
|
vault migrate ./photos # like add, then delete the originals
|
||||||
|
vault mirror https://example.com/ # crawl a site into example.com/, links rewritten
|
||||||
vault list # everything
|
vault list # everything
|
||||||
vault list work # everything under work/
|
vault list work # everything under work/
|
||||||
vault get work/2026/report.pdf # writes ./report.pdf
|
vault get work/2026/report.pdf # writes ./report.pdf
|
||||||
|
|||||||
@@ -561,6 +561,12 @@ downloads it (as Chrome, so fussy sites play along) and stores it under the
|
|||||||
last part of the address, such as \file{report.pdf}. The download sits in a
|
last part of the address, such as \file{report.pdf}. The download sits in a
|
||||||
private temporary file only until it's encrypted.
|
private temporary file only until it's encrypted.
|
||||||
|
|
||||||
|
\subsection{Copying a website: \texttt{mirror}}
|
||||||
|
\cmd{mirror} takes an \file{http://} or \file{https://} address and copies
|
||||||
|
the whole site, one request at a time, into a folder named after its host.
|
||||||
|
Links inside pages and stylesheets are rewritten to relative paths, so the
|
||||||
|
copy still browses once you get it out. Only the same host is followed.
|
||||||
|
|
||||||
\subsection{Moving in: \texttt{migrate}}
|
\subsection{Moving in: \texttt{migrate}}
|
||||||
\cmd{migrate} works exactly like \cmd{add}, but deletes each original once
|
\cmd{migrate} works exactly like \cmd{add}, but deletes each original once
|
||||||
it's safely stored, then tidies away any folders left empty. It won't take a
|
it's safely stored, then tidies away any folders left empty. It won't take a
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.TH VAULT-ADD 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-add \- encrypt files, directories or URLs into the vault
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault add
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.IR FILE | DIR | URL
|
||||||
|
.RI [ FOLDER ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Encrypts a file into the vault, named after its base name. A directory is
|
||||||
|
added recursively, keeping its structure beneath its own name; a directory
|
||||||
|
named "." or ".." adds its contents directly. Non-regular files, the vault
|
||||||
|
and the key are skipped. A stored name that already exists is replaced.
|
||||||
|
.PP
|
||||||
|
An \fBhttp://\fR or \fBhttps://\fR \fIURL\fR is downloaded with a Chrome user
|
||||||
|
agent and stored under the last element of its final (post-redirect) path, or
|
||||||
|
the host name if there is none. Only a 200 response is accepted.
|
||||||
|
.PP
|
||||||
|
\fIFOLDER\fR places the result inside a folder in the vault. File contents
|
||||||
|
are cut into 4 MiB chunks, the last bucket-padded.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
vault add report.pdf work/2026
|
||||||
|
vault add ./photos
|
||||||
|
vault add https://example.com/paper.pdf papers
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-migrate (1),
|
||||||
|
.BR vault\-mirror (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.TH VAULT-GENKEY 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-genkey \- create a new vault key
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault genkey
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Creates the key file, prompting twice for a passphrase, then for an optional
|
||||||
|
decoy passphrase (empty for none). Both passphrase slots are sealed with
|
||||||
|
Argon2id and XChaCha20-Poly1305, are the same size, and are stored in random
|
||||||
|
order; the second holds either a decoy identity or random bytes. It refuses to
|
||||||
|
overwrite an existing file. No seed or private key is ever written unencrypted.
|
||||||
|
.PP
|
||||||
|
When \fBVAULT_PASSPHRASE\fR is set, no prompts are made and the decoy comes
|
||||||
|
from \fBVAULT_DECOY_PASSPHRASE\fR, or is omitted.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Where to write the key. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.TP
|
||||||
|
.B VAULT_DECOY_PASSPHRASE
|
||||||
|
Decoy passphrase; must differ from the passphrase.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-init (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.TH VAULT-GET 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-get \- decrypt a stored file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault get
|
||||||
|
.RB [ \-o
|
||||||
|
.IR OUTPUT ]
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.I NAME
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Decrypts \fINAME\fR. Nothing appears at the output path until every object has
|
||||||
|
authenticated. The stored mode and modification time are restored. It refuses
|
||||||
|
to overwrite an existing file. Takes a shared lock.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-o " OUTPUT"
|
||||||
|
Output path. Default: the base name of \fINAME\fR in the current directory.
|
||||||
|
.PP
|
||||||
|
See also \fB\-\-key\fR and \fB\-\-vault\fR in
|
||||||
|
.BR vault (1).
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-list (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.TH VAULT-INIT 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-init \- create an empty vault
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault init
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.RB [ \-\-filler
|
||||||
|
.IR SIZE ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Creates the vault directory, with \fBrecords/\fR and \fBobjects/\fR each
|
||||||
|
sharded into 256 subdirectories. It writes at least \fISIZE\fR bytes of
|
||||||
|
unreadable filler so that real files cannot be told apart from it by anyone
|
||||||
|
lacking the right passphrase. Filler is always written, even when \fISIZE\fR is 0.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.TP
|
||||||
|
.BI \-\-filler " SIZE"
|
||||||
|
Minimum filler, in bytes, with an optional K, M, G or T suffix (powers of
|
||||||
|
1024), e.g. 500M or 2G. Default: 0.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-genkey (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.TH VAULT-LIST 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-list \- list files in the vault
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault list
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.RI [ PREFIX ]
|
||||||
|
.br
|
||||||
|
.B vault ls
|
||||||
|
\&...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Prints one line per stored file, sorted by name: human-readable size,
|
||||||
|
modification time (local, minute resolution) and name. With \fIPREFIX\fR only
|
||||||
|
that file, or files under that folder, are shown. Records the passphrase
|
||||||
|
cannot open are skipped silently. Takes a shared lock.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-get (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.TH VAULT-MIGRATE 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-migrate \- move files into the vault, deleting the originals
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault migrate
|
||||||
|
.RB [ \-\-yes ]
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.IR FILE | DIR
|
||||||
|
.RI [ FOLDER ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Works like
|
||||||
|
.BR vault\-add (1)
|
||||||
|
but deletes each original once it is stored, then any directories left empty.
|
||||||
|
Directories still holding skipped files stay. A URL cannot be migrated.
|
||||||
|
.PP
|
||||||
|
Before storing anything it lists the vault names that would be overwritten and
|
||||||
|
asks for confirmation on the terminal. With no terminal the answer is no, and
|
||||||
|
nothing changes.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-\-yes
|
||||||
|
Overwrite without asking.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-add (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
.TH VAULT-MIRROR 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-mirror \- mirror a website into an encrypted vault
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault mirror
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.I URL
|
||||||
|
.RI [ FOLDER ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B vault mirror
|
||||||
|
crawls the website at
|
||||||
|
.I URL
|
||||||
|
and stores every page and asset it finds in the vault, beneath
|
||||||
|
.IR FOLDER / HOST
|
||||||
|
(or just
|
||||||
|
.I HOST
|
||||||
|
if no folder is given).
|
||||||
|
.PP
|
||||||
|
The crawl is single threaded: one request at a time, in breadth-first order.
|
||||||
|
It follows only links on the same host as
|
||||||
|
.IR URL ,
|
||||||
|
and requests are sent with a Chrome user agent string.
|
||||||
|
.PP
|
||||||
|
Links in HTML
|
||||||
|
.RB ( href ,
|
||||||
|
.BR src ,
|
||||||
|
.BR action
|
||||||
|
and
|
||||||
|
.B poster
|
||||||
|
attributes) and in CSS
|
||||||
|
.RB ( url(...) )
|
||||||
|
that point into the mirror are rewritten to relative paths, so the copy
|
||||||
|
browses correctly once fetched out of the vault. Links to other hosts, and
|
||||||
|
fragments, are left as they were.
|
||||||
|
.PP
|
||||||
|
Pages are named after their URL path. A path that is empty or ends in
|
||||||
|
.B /
|
||||||
|
becomes
|
||||||
|
.BR index.html ;
|
||||||
|
a query string is appended as
|
||||||
|
.BI _ query
|
||||||
|
(URL-escaped). Anything already stored under the same name is replaced.
|
||||||
|
.PP
|
||||||
|
A page that fails to download, or answers with anything but
|
||||||
|
.BR "200 OK" ,
|
||||||
|
is reported on standard error and skipped; the crawl carries on.
|
||||||
|
The vault is locked exclusively for the whole run.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH EXAMPLES
|
||||||
|
Mirror a site into the vault under
|
||||||
|
.BR example.com/ :
|
||||||
|
.PP
|
||||||
|
.RS
|
||||||
|
vault mirror https://example.com/
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
Mirror it under a folder, then fetch the front page:
|
||||||
|
.PP
|
||||||
|
.RS
|
||||||
|
vault mirror https://example.com/ archive/2026
|
||||||
|
.br
|
||||||
|
vault get archive/2026/example.com/index.html
|
||||||
|
.RE
|
||||||
|
.SH LIMITATIONS
|
||||||
|
Links are found by pattern matching, not by parsing HTML: URLs assembled by
|
||||||
|
scripts, and
|
||||||
|
.B srcset
|
||||||
|
lists, are neither followed nor rewritten. There is no depth limit, rate
|
||||||
|
limit or robots.txt handling; only mirror sites you may copy.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.TH VAULT-MV 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-mv \- rename a stored file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault mv
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.I OLD NEW
|
||||||
|
.br
|
||||||
|
.B vault rename
|
||||||
|
\&...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Renames \fIOLD\fR to \fINEW\fR, which may be in another folder. Fails if
|
||||||
|
\fINEW\fR exists or is not a valid vault path. File contents are not
|
||||||
|
re-encrypted.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-rm (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.TH VAULT-RM 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault-rm \- delete a stored file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault rm
|
||||||
|
.RB [ \-\-key
|
||||||
|
.IR PATH ]
|
||||||
|
.RB [ \-\-vault
|
||||||
|
.IR DIR ]
|
||||||
|
.I NAME
|
||||||
|
.br
|
||||||
|
.B vault delete
|
||||||
|
\&...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Deletes the stored file \fINAME\fR from the vault. It does not ask for
|
||||||
|
confirmation.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR vault (1),
|
||||||
|
.BR vault\-list (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
.TH VAULT 1 "September 2026" "vault" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
vault \- post-quantum encrypted file vault
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B vault
|
||||||
|
.I command
|
||||||
|
.RI [ options ]
|
||||||
|
.RI [ arguments ]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B vault
|
||||||
|
stores files in a directory of encrypted, fixed-size records and chunks.
|
||||||
|
Encryption is a hybrid of FrodoKEM-640 and ML-KEM-768 with ChaCha20-Poly1305.
|
||||||
|
The key file is protected by an Argon2id-derived passphrase key.
|
||||||
|
File names, sizes and contents are only visible to a holder of the key and
|
||||||
|
passphrase. Ciphertext sizes and timestamps are normalised to hide
|
||||||
|
metadata from the storage host.
|
||||||
|
.PP
|
||||||
|
Options may appear before, between or after the arguments.
|
||||||
|
.SH COMMANDS
|
||||||
|
.TP
|
||||||
|
.BR vault\-genkey (1)
|
||||||
|
Create a new passphrase-protected key file.
|
||||||
|
.TP
|
||||||
|
.BR vault\-init (1)
|
||||||
|
Create a new, empty vault.
|
||||||
|
.TP
|
||||||
|
.BR vault\-add (1)
|
||||||
|
Encrypt a file, directory or downloaded URL into the vault.
|
||||||
|
.TP
|
||||||
|
.BR vault\-migrate (1)
|
||||||
|
Like add, then delete each original.
|
||||||
|
.TP
|
||||||
|
.BR vault\-mirror (1)
|
||||||
|
Crawl a website into the vault.
|
||||||
|
.TP
|
||||||
|
.BR vault\-list (1)
|
||||||
|
List stored files (alias:
|
||||||
|
.BR ls ).
|
||||||
|
.TP
|
||||||
|
.BR vault\-get (1)
|
||||||
|
Decrypt a stored file.
|
||||||
|
.TP
|
||||||
|
.BR vault\-mv (1)
|
||||||
|
Rename a stored file (alias:
|
||||||
|
.BR rename ).
|
||||||
|
.TP
|
||||||
|
.BR vault\-rm (1)
|
||||||
|
Delete a stored file (alias:
|
||||||
|
.BR delete ).
|
||||||
|
.TP
|
||||||
|
.B help
|
||||||
|
Print usage.
|
||||||
|
.SH OPTIONS
|
||||||
|
These apply to every command.
|
||||||
|
.TP
|
||||||
|
.BI \-\-key " PATH"
|
||||||
|
Key file. Default: $VAULT_KEY, else ~/.vault/key.
|
||||||
|
.TP
|
||||||
|
.BI \-\-vault " DIR"
|
||||||
|
Vault directory. Default: $VAULT_DIR, else ~/.vault/store.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B VAULT_KEY
|
||||||
|
Default key file.
|
||||||
|
.TP
|
||||||
|
.B VAULT_DIR
|
||||||
|
Default vault directory.
|
||||||
|
.TP
|
||||||
|
.B VAULT_PASSPHRASE
|
||||||
|
Passphrase, instead of prompting on the terminal.
|
||||||
|
.TP
|
||||||
|
.B VAULT_DECOY_PASSPHRASE
|
||||||
|
Decoy passphrase used by
|
||||||
|
.BR "vault genkey" .
|
||||||
|
.SH LOCKING
|
||||||
|
Commands that change the vault hold an exclusive lock on
|
||||||
|
.IR DIR/.lock ;
|
||||||
|
readers hold a shared lock. The lock is taken after the passphrase prompt.
|
||||||
|
.SH DECOY PASSPHRASE
|
||||||
|
.B genkey
|
||||||
|
can add a second passphrase. It unlocks a separate identity, so every
|
||||||
|
command run with it sees and changes only the decoy files, and the real
|
||||||
|
files stay invisible. The key file does not reveal whether a decoy exists,
|
||||||
|
and
|
||||||
|
.B init
|
||||||
|
fills each vault with unreadable filler, so hidden files look like filler to
|
||||||
|
anyone holding only the decoy passphrase.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
0 on success; 1 on error (diagnostics on standard error); 2 when run with no command.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
vault genkey
|
||||||
|
vault init \-\-filler 500M
|
||||||
|
vault add report.pdf work/2026
|
||||||
|
vault list work
|
||||||
|
vault get work/2026/report.pdf \-o /tmp/report.pdf
|
||||||
|
.fi
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I ~/.vault/key
|
||||||
|
Default key file.
|
||||||
|
.TP
|
||||||
|
.I ~/.vault/store
|
||||||
|
Default vault directory.
|
||||||
|
.SH AUTHOR
|
||||||
|
Andy Dixon <vault@dixon.cx>, https://www.dixon.cx
|
||||||
@@ -32,6 +32,9 @@ const usage = `Usage:
|
|||||||
[--yes] it is stored; asks before overwriting
|
[--yes] it is stored; asks before overwriting
|
||||||
anything already in the vault, unless
|
anything already in the vault, unless
|
||||||
--yes is given
|
--yes is given
|
||||||
|
vault mirror URL [FOLDER] crawl a website (same host only, one request
|
||||||
|
at a time) into FOLDER/HOST, rewriting its
|
||||||
|
links to relative paths
|
||||||
vault list [PREFIX] list stored files
|
vault list [PREFIX] list stored files
|
||||||
vault get NAME [-o OUTPUT] decrypt a stored file (default: its base name)
|
vault get NAME [-o OUTPUT] decrypt a stored file (default: its base name)
|
||||||
vault mv OLD NEW rename a stored file
|
vault mv OLD NEW rename a stored file
|
||||||
@@ -130,6 +133,19 @@ func main() {
|
|||||||
overwrite = func([]string) bool { return true }
|
overwrite = func([]string) bool { return true }
|
||||||
}
|
}
|
||||||
check(addPath(s, cat, pos[0], dest, cmd == "migrate", skip, overwrite))
|
check(addPath(s, cat, pos[0], dest, cmd == "migrate", skip, overwrite))
|
||||||
|
case "mirror":
|
||||||
|
want(1, 2)
|
||||||
|
if !isURL(pos[0]) {
|
||||||
|
fail("mirror needs an http:// or https:// URL")
|
||||||
|
}
|
||||||
|
dest := ""
|
||||||
|
if len(pos) == 2 {
|
||||||
|
dest = strings.Trim(pos[1], "/")
|
||||||
|
}
|
||||||
|
s := open(*keyPath, *vaultDir, true)
|
||||||
|
cat, err := s.catalogue()
|
||||||
|
check(err)
|
||||||
|
check(mirror(s, cat, pos[0], dest))
|
||||||
case "list", "ls":
|
case "list", "ls":
|
||||||
want(0, 1)
|
want(0, 1)
|
||||||
prefix := ""
|
prefix := ""
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// Copyright 2026 The vault Authors.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Attribute values in HTML, and url(...) in CSS: group 1 is everything
|
||||||
|
// up to the URL, group 2 is the URL.
|
||||||
|
htmlURLs = regexp.MustCompile(`(?i)(\b(?:href|src|action|poster)\s*=\s*["'])([^"']*)`)
|
||||||
|
cssURLs = regexp.MustCompile(`(?i)(url\(\s*["']?)([^"')\s]+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// mirror crawls the site at rawURL, one request at a time, staying on its
|
||||||
|
// host, and adds every page and asset beneath dest/host. Links in HTML and
|
||||||
|
// CSS that point into the mirror are rewritten to relative paths, so the
|
||||||
|
// copy browses correctly once fetched out of the vault. A page that fails
|
||||||
|
// is reported and skipped.
|
||||||
|
// ponytail: regex rewriting, so links built by scripts or in srcset are left
|
||||||
|
// as they were; swap in golang.org/x/net/html if that matters.
|
||||||
|
func mirror(s *store, cat map[string][]*record, rawURL, dest string) error {
|
||||||
|
start, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
host := strings.ToLower(start.Host)
|
||||||
|
root := path.Join(dest, host)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
queue := []*url.URL{start}
|
||||||
|
seen[key(start)] = true
|
||||||
|
|
||||||
|
for len(queue) > 0 {
|
||||||
|
u := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
err := mirrorOne(s, cat, u, host, root, func(l *url.URL) {
|
||||||
|
if k := key(l); !seen[k] {
|
||||||
|
seen[k] = true
|
||||||
|
queue = append(queue, l)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "skipping %s: %v\n", u, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// key identifies a page independent of its fragment.
|
||||||
|
func key(u *url.URL) string {
|
||||||
|
c := *u
|
||||||
|
c.Fragment = ""
|
||||||
|
return c.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// localPath is where u lives inside the mirror, relative to its root.
|
||||||
|
func localPath(u *url.URL) string {
|
||||||
|
p := strings.TrimPrefix(u.Path, "/")
|
||||||
|
if p == "" || strings.HasSuffix(p, "/") {
|
||||||
|
p += "index.html"
|
||||||
|
}
|
||||||
|
if u.RawQuery != "" {
|
||||||
|
p += "_" + url.QueryEscape(u.RawQuery)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// relative gives the link from the file "from" to the file "to", both
|
||||||
|
// slash-separated paths relative to the same root.
|
||||||
|
func relative(from, to string) string {
|
||||||
|
f := strings.Split(path.Dir(from), "/")
|
||||||
|
t := strings.Split(to, "/")
|
||||||
|
if f[0] == "." {
|
||||||
|
f = nil
|
||||||
|
}
|
||||||
|
for len(f) > 0 && len(t) > 1 && f[0] == t[0] {
|
||||||
|
f, t = f[1:], t[1:]
|
||||||
|
}
|
||||||
|
return strings.Repeat("../", len(f)) + strings.Join(t, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mirrorOne(s *store, cat map[string][]*record, u *url.URL, host, root string, enqueue func(*url.URL)) error {
|
||||||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", chromeUA)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("%s", resp.Status)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
self := localPath(u)
|
||||||
|
// rewrite maps a link found in the page to a relative one, queueing it.
|
||||||
|
rewrite := func(re *regexp.Regexp, text string) string {
|
||||||
|
return re.ReplaceAllStringFunc(text, func(m string) string {
|
||||||
|
sub := re.FindStringSubmatch(m)
|
||||||
|
l, err := u.Parse(html.UnescapeString(sub[2]))
|
||||||
|
if err != nil || (l.Scheme != "http" && l.Scheme != "https") || strings.ToLower(l.Host) != host {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
frag := l.Fragment
|
||||||
|
l.Fragment = ""
|
||||||
|
if l.Path == "" && l.RawQuery == "" {
|
||||||
|
l.Path = "/"
|
||||||
|
}
|
||||||
|
enqueue(l)
|
||||||
|
r := relative(self, localPath(l))
|
||||||
|
if frag != "" {
|
||||||
|
r += "#" + frag
|
||||||
|
}
|
||||||
|
return sub[1] + r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
switch ct := strings.ToLower(resp.Header.Get("Content-Type")); {
|
||||||
|
case strings.HasPrefix(ct, "text/html"):
|
||||||
|
body = []byte(rewrite(cssURLs, rewrite(htmlURLs, string(body))))
|
||||||
|
case strings.HasPrefix(ct, "text/css"):
|
||||||
|
body = []byte(rewrite(cssURLs, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp("", "vault-mirror-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp.Name())
|
||||||
|
defer tmp.Close()
|
||||||
|
if _, err := tmp.Write(body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return addOne(s, cat, tmp.Name(), path.Join(root, self), false)
|
||||||
|
}
|
||||||
@@ -373,3 +373,60 @@ func TestAddTree(t *testing.T) {
|
|||||||
t.Fatalf("downloaded %q", b)
|
t.Fatalf("downloaded %q", b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMirror crawls a two-page site: links in HTML and CSS become relative,
|
||||||
|
// off-host links stay, and everything is stored under host/.
|
||||||
|
func TestMirror(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
keyPath := filepath.Join(dir, "key")
|
||||||
|
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := initStore(filepath.Join(dir, "store"), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := newTestStore(t, keyPath, "pass")
|
||||||
|
var srv *httptest.Server
|
||||||
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.UserAgent() != chromeUA {
|
||||||
|
http.Error(w, "bad agent", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/":
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
io.WriteString(w, `<a href="`+srv.URL+`/sub/p.html#x">p</a> <a href="https://elsewhere.example/">e</a> <link href='/s.css'>`)
|
||||||
|
case "/sub/p.html":
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
io.WriteString(w, `<a href="/">home</a> <img src="../img/a.png">`)
|
||||||
|
case "/s.css":
|
||||||
|
w.Header().Set("Content-Type", "text/css")
|
||||||
|
io.WriteString(w, `body{background:url(/img/a.png)}`)
|
||||||
|
case "/img/a.png":
|
||||||
|
io.WriteString(w, "png")
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
cat, _ := s.catalogue()
|
||||||
|
if err := mirror(s, cat, srv.URL, "m"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
host := srv.Listener.Addr().String()
|
||||||
|
for name, want := range map[string]string{
|
||||||
|
"m/" + host + "/index.html": `<a href="sub/p.html#x">p</a> <a href="https://elsewhere.example/">e</a> <link href='s.css'>`,
|
||||||
|
"m/" + host + "/sub/p.html": `<a href="../index.html">home</a> <img src="../img/a.png">`,
|
||||||
|
"m/" + host + "/s.css": `body{background:url(img/a.png)}`,
|
||||||
|
"m/" + host + "/img/a.png": "png",
|
||||||
|
} {
|
||||||
|
out := filepath.Join(dir, "out")
|
||||||
|
if err := s.get(name, out); err != nil {
|
||||||
|
t.Fatal(name, err)
|
||||||
|
}
|
||||||
|
if b, _ := os.ReadFile(out); string(b) != want {
|
||||||
|
t.Errorf("%s = %q, want %q", name, b, want)
|
||||||
|
}
|
||||||
|
os.Remove(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user