rveil: encrypted, padded archives on an rclone remote
October 23, 2026


rveil puts files and folders on an untrusted remote, such as Google Drive, in a form the remote can store but not read. Each file or folder becomes one archive, encrypted under its own key. The keys are derived from a master key that lives in 1Password. The remote sees a handful of random-looking ids and a rough idea of their sizes, and nothing else.
Pipeline
An archive is built in one fixed construction, with no options to choose from:
tar → zstd → Padmé padding → age (scrypt passphrase)
- tar (libarchive’s bsdtar) packs one file or folder, under its name only, never its original path.
- zstd compresses it, with a window small enough that a stock
zstd -dneeds no extra flags. - Padmé padding (Nikitin et al., PETS 2019) rounds the size up to a bucket that reveals only O(log log n) bits of the length, for at most 12% overhead. The padding is made of zstd skippable frames, so a standard decoder skips it without knowing it’s there.
- age encrypts the result with ChaCha20-Poly1305 in 64 KiB chunks, so any change or truncation fails to decrypt.
Everything is symmetric, so a future quantum computer doesn’t help anyone who keeps the ciphertext around.
Keys
A master key is 32 random bytes. Each archive gets an id, <key id>.<130 random bits in base32>, and its passphrase is
base64url(HKDF-SHA256(ikm = master key, salt = "rveil/v1", info = id))
This binds every passphrase to its archive: handing one out exposes nothing else, and a renamed or swapped file simply doesn’t decrypt. Since the passphrase is already a 256-bit random value, age’s scrypt stretching adds nothing, so rveil pins its work factor to the minimum and refuses anything else.
Master keys are stored in a 1Password vault and are held in memory only for the life of one command. They reach the 1Password CLI as JSON on stdin, never in arguments or the environment. Rotating is rveil keys new followed by rveil rekey, which swaps only the encryption layer, remote to remote, without recompressing or touching the local disk.
Verified uploads
While sealing, rveil decrypts the archive again in-process and checks it against the content. The upload then goes to a .partial file, and the SHA-256 the remote reports must match the bytes that were sent before it’s moved into place. Together these prove that the copy on the remote opens to the right content, without downloading it back. A file without .partial on the remote is therefore always complete.
The index
The list of archives (names, sizes, hashes, dates) is itself an archive, index.age, sealed with the same pipeline under the current key. rveil check compares it with what’s actually on the remote: it cleans up unfinished uploads, re-adds archives the index has lost track of (their name is inside them), and drops entries whose archive is gone. check --deep also decrypts everything.
Recovery with stock tools
The format is documented well enough to recover everything without rveil: OpenSSL 3 derives the passphrase, then
age -d <id>.age | zstd -d | tar x
The integration tests check exactly this path, along with extraction on a machine that has only GNU tar.
Sharing
rveil share makes a public link for one archive and prints a message with the link, that archive’s passphrase, and a one-line command. The command fetches a small script from this site, which downloads the matching rveil release, checks it against a checksum baked into the script, and opens the archive. The recipient needs only bash, curl and tar. Deleting the archive kills the link; rekeying it cuts off the passphrase.
Threat model
The remote sees archive ids, the key id each was made under, how many archives there are, their padded sizes and upload times. It can delete archives or put back an older index, and rveil check notices both. Everything else is encrypted and authenticated.
Install
rveil is written in Go and needs rclone, bsdtar and, for the default key store, the 1Password CLI.
go install davidefiocchi.com/rveil@latest