- No client-side encryption: plaintext content-addressed remote (SHA-256 names), no backup key, no cryptography dependency (server disk encryption is the trust model). - Filters back user data: images, archives, databases, PDFs accepted; 10 MiB cap; binary sniffing removed; temp names hardened (~$, #..#, .temp). - SQLite zero-error policy: backup-API snapshots + integrity_check, journal folding, locked/corrupt loud skips, verified restores. - Recovery: reindex from manifests, remote adopt, on-demand blob fetch, 5-day retention + thinning, GC, date-guarded remote purge, metrics. - Scheduler with WebDAV quota signal and 70% pressure backstop (floor kept). - 37 tests incl. live-monitor capture safety and DB safety.
23 KiB
Versiond should encrypt locally, hide metadata
For a local Linux daemon that stages small files and syncs to a dumb WebDAV store, the winning pattern is Kopia-style client-side envelope encryption with AES-256-GCM as default and ChaCha20-Poly1305 as portable fallback, applied after local spool-compression in the strict order hash then compress then encrypt, with per-blob random nonces and per-content keys derived by HMAC/HKDF. Restic proves that bespoke AES-256-CTR plus Poly1305-AES in Encrypt-then-MAC with 16-byte random IV per file works (Source), Borg 1.x proves that AES-256-CTR plus HMAC-SHA256 with 8-byte counter IVs and reservation tracking scales poorly to multiple writers (Source), and Borg 2 plus Kopia converge on standard AEAD precisely to escape that complexity with AES-256-OCB or ChaCha20-Poly1305 with session keys (Source) and AES256-GCM-HMAC-SHA256 default, immutable after creation (Source). Duplicity delegates entirely to GnuPG OpenPGP hybrid encryption of tar volumes (Source) and therefore offers no daemon-friendly manifest story. Because WebDAV provides no compute, no trustworthy mtime or hash, and no append-only enforcement, versiond must do what Kopia already does for WebDAV-class stores — encrypt, pack into 20–40 MB randomly named blobs with server-opaque indexes (Source), authenticate manifests with the same keys as data, and add rollback resistance through a local monotonic counter plus server-side versioning rather than cryptography alone.
AES-GCM and ChaCha20-Poly1305 replace bespoke CTR-MAC designs
Restic encrypts every repository object except key wrappers as IV(16) || CIPHERTEXT || MAC(16) for 32 bytes overhead with a fresh CSPRNG nonce per encryption (Source, and derives its three subkeys by splitting 64 scrypt-derived bytes into a 32-byte AES-256 key plus 16-byte AES key k plus 16-byte Poly1305 key r (Source). Pack files then carry multiple independently encrypted blobs plus an encrypted header and little-endian header length, with blob type bits distinguishing plain versus compressed data and tree objects (Source). External review found the construction sane, quoting that encryption is a first-class feature and the deduplication trade-off is worth it (Source), but the design is deliberately non-standard and carries Poly1305-AES masking complexity that modern AEAD avoids. Borg 1.x chose a different bespoke path with AES-CTR-256 plus HMAC-SHA256 in Encrypt-then-MAC (Source) and a TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT envelope using two different keys (Source), where only 8 stored nonce bytes cap capacity at 264 times 16 bytes** (Source) and uniqueness depends on reserving counter ranges by adding 4 GiB divided by 16 bytes and persisting through SaveFile (Source). That reservation breaks under independent writers, with Borg documenting that with multiple independent clients on the same repo Borg fails to provide confidentiality (Source), which is exactly why Borg 2 abandons counters for session keys and standard AEAD. Borg 2 offers aes256-ocb, chacha20-poly1305, authenticated-only and none modes (Source) to get 256-bit authenticated encryption client-side (Source) with goals stated as getting rid of CTR, using session keys, and moving to Argon2 plus AES-OCB and ChaCha20-Poly1305 (Source). Kopia made the same modern choice earlier with DefaultAlgorithm AES256-GCM-HMAC-SHA256 (Source) and an explicit option for CHACHA20-POLY1305-HMAC-SHA256 fixed at creation (Source), where per-content keys come from HMAC-SHA256 derivation with 28 bytes overhead (Source) and the repository format block itself is sealed with Ke equals HKDF-SHA256 of Km over UniqueID for AES plus AD equals HKDF for CHECKSUM (Source). For versiond the implication is direct: use a Go-available AEAD with random 12-byte nonces per blob and HKDF-separated data versus manifest keys, pick AES-GCM where hardware AES favours HMAC-SHA256 paths and portable CPUs favour BLAKE2/ChaCha thinking already documented by Borg (Source), and never invent a CTR-plus-MAC variant or shell out to GnuPG per sync like Duplicity does.
| System | Cipher and integrity | Nonce and key separation |
|---|---|---|
| restic | AES-256-CTR + Poly1305-AES Encrypt-then-MAC (Source) | 16-byte random IV per file, 32 B overhead (Source) |
| Borg 1.x | AES-CTR-256 + HMAC-SHA256 or keyed BLAKE2b-256 (Source) | 64-bit counter with reservation commit (Source) |
| Borg 2 | AES-256-OCB or ChaCha20-Poly1305 AEAD (Source) | Session keys to avoid global counter tracking (Source) |
| Kopia | AES256-GCM-HMAC-SHA256 default, ChaCha20 variant optional (Source) | Per-content HMAC-derived keys, format block with Ke and AD separation (Source) |
| Duplicity | External GnuPG OpenPGP hybrid with per-recipient session keys (Source) | Random session key s plus Enc_ri(s) per recipient, cipher negotiated by GnuPG (Source) |
In transit the same client-side property carries over, which matters for dumb WebDAV over plain HTTP. Borg assumes client trusted and repository untrusted with full read-write man-in-the-middle (Source) and runs RPC over system SSH with no own network code (Source), while Kopia states that all repository features are implemented client-side without any need for a custom server, thus encryption keys never leave the client (Source). Restic promises that everything except informational key-file metadata is encrypted and authenticated (Source) and Duplicity claims volumes will be safe from spying and modification by the server via librsync plus GnuPG (Source). Versiond therefore gets confidentiality even over untrusted HTTP, but still needs TLS or SSH tunnelling to blunt traffic analysis and to stop a network attacker from deleting or replaying blobs undetected, since object MACs detect tampering but not deletion.
Hashing before compressing before encrypting preserves deduplication
All three content-defined systems converge on split then hash then compress then encrypt then pack, and every deviation is documented as an anti-pattern. Restic hashes plaintext for identity in saveBlob before zstd compression and Seal, and compresses unpacked index and snapshot JSON before encryption (Source), explicitly noting that unpacked files like lock, index and snapshot files are also compressed before encryption (Source). Borg follows id equals MAC of id_key over data, then compress, then AEAD encrypt with id and header as associated data (Source) and documents that compression is applied after deduplication, thus different methods do not influence deduplication (Source). Kopia pipelines splits into chunks, hash, compare, if new compress chunk, encrypt, pack (Source) after deliberately rewiring from read-split-compress-hash-store to read-split-hash-compress-store to stop compressor drift from changing IDs (Source). The rationale is physical: ciphertext from randomized encryption does not compress and destroys equality, so encryption must be last while hashing must be first, and compressing whole files before chunking avalanches small edits through the compressor and shifts cut points (Source). Kopia even quantifies the accepted cost of chunk-then-compress versus whole-file compression with s2 and gzip ratios differing by a few points in exchange for shift-resilient dedup (Source). Versiond spool-compressing many small files locally should copy this exactly: chunk the spool with a content-defined splitter, hash plaintext for the dedup index, compress each new chunk independently, then AEAD-encrypt.
Codec handling differs in ways versiond can borrow selectively. Restic repository v2 supports zstandard only for data and tree blobs (Source) negotiated per run as off, fastest, auto default, better, max mapped to klauspost compress levels (Source), recording compressed blobs with an extra plaintext length field plus uncompressed_length in the index (Source) so that compressed and uncompressed blobs may be mixed in one pack without re-uploading old data (Source). Borg records ctype plus clevel plus csize plus size per object for none, lz4, zstd, zlib and lzma (Source), defaults to lz4 and explicitly allows the first writer to fix a chunk's stored compression while later runs mix freely (Source), plus wrappers for auto selection and obfuscate padding with about 12 percent overhead (Source). Kopia disables compression by default, controls it per policy path, offers a wide menu from s2-default through pgzip to zstd-better-compression with zstd recommended (Source), and since index v2 keeps per-content compression IDs in manager bookkeeping visible via content list and stats rather than in the ID itself (Source), so dedup is unaffected because hashing precedes compression (Source). For versiond the practical synthesis is to store one zstd default for text-like spool files plus s2 for speed-sensitive paths, record codec and level outside the content ID exactly as Borg and Kopia do, freely mix codecs across blobs, and skip compression when output grows, mirroring Kopia storing the original when compressed output is larger (Source). Integrity then comes free from the AEAD tag plus content addressing: restic verifies filenames as hex SHA-256 of ciphertext with sha256sum and checks structure with optional read-data payload reads (Source), Borg binds chunk ID as associated data so the server cannot swap content under an ID (Source), and Kopia packs blocks with a trailing local index plus top-level index for rebuilds (Source).
Envelope encryption with scrypt or Argon2id decides recoverability
Every system separates a random master secret from a password-derived wrapping key, but parameters and ergonomics decide whether versiond operators survive key loss. Restic pins its key-file example to scrypt N equals 65536, r equals 8, p equals 1 and rejects any other KDF (Source), calibrating stronger parameters only when adding keys (Source). Borg 1.x wraps keys with PBKDF2-HMAC-SHA256 plus AES-CTR with zero IV and HMAC (Source), while Borg 2 derives a 256-bit key-encryption key with Argon2 plus random 256-bit salt then ChaCha20-Poly1305 with constant IV zero (Source) and makes Argon2id the default with PBKDF2 kept for compatibility (Source). Kopia seals its format block with Km equals PBKDF over UniqueID then Ke and AD via HKDF-SHA256 for AES and CHECKSUM domains (Source), documents only scrypt-65536-8-1 as supported at the moment (Source), and in 2025 added configurable PBKDF with 600000 iterations default for PBKDF2 and 64 MB default for scrypt (Source). Ancillary systems sharpen the contrast: rclone crypt derives 80 bytes via scrypt N equals 16384, r equals 8, p equals 1 (Source) and Tarsnap generates keys locally, using scrypt only to optionally wrap the key file (Source). Versiond should therefore follow Borg 2 on new installs with Argon2id where libargon2 is available and otherwise scrypt-65536-8-1, always with a random 32-byte UniqueID salt that doubles as HKDF info to prevent cross-repo correlation (Source).
Storage location and rotation complete the choice. Restic keeps keys directory JSON in the repo with hostname, KDF params, salt and encrypted data (Source), Kopia keeps a format blob with UniqueID, keyAlgo and encryptedBlockFormat in storage plus a local connection config without the password (Source), and Borg alone offers repokey in the repo versus keyfile under home with move via key change-location (Source). True rotation by re-encrypting data exists nowhere; all rotation re-wraps the same secret, whether through restic key add and passwd managing multiple access keys sharing one master (Source), Borg change-passphrase only re-locking the same secrets (Source), Kopia change-password re-encrypting the format block while still connected (Source), or rclone requiring full re-upload to change passwords (Source). No system documents native Shamir sharing. Loss is therefore uniformly fatal by design: restic repeats that losing your password means data is irrecoverably lost (Source), Kopia states there is no way to recover a forgotten password because only you know it (Source), Borg demands both key and passphrase with offsite export (Source), and Tarsnap warns that the key file contains the only copy (Source). For versiond this means shipping multi-key wrapping from day one so a daemon key and an offline admin recovery key share the same master, exporting with paper printouts with per-line checksums or QR plus separate passphrase storage as Borg documents (Source), caching the daemon password in the Linux keyring only as Kopia does transiently (Source), and documenting that a leaked master forces new-repo migration because revocation without re-encrypting the whole repository is impossible (Source).
Random packing and padding hide far more than encryption alone
Encryption hides bytes but every system leaks sizes, counts, timing and directory shape to a curious WebDAV host, and versiond must treat that as its main residual risk. Restic filenames are lowercase hex storage IDs verifiable by sha256sum (Source) under predictable config, keys, locks, snapshots, index and data prefixes (Source), with the server able to infer which packs probably contain trees via access patterns and infer backup sizes via timestamps (Source), including pre-0.18 chunk-size fingerprinting only partly fixed by randomly assigning chunks to pack files (Source). Borg is explicit that the repository does not hide the size of chunks and that single-chunk small files plus inode-order proximity let an observer correlate neighbours unless the operator enables the obfuscate pseudo-compressor that pads with zero bytes (Source). Rclone crypt is weakest here by design, stating it does not encrypt file length within 16 bytes nor modification time used for syncing (Source) and encrypting names deterministically with EME-AES-256 plus base32 so identical names yield identical ciphertexts (Source), while plain WebDAV does not support modified times nor hashes except vendor extensions (Source). Kopia leaks least by default because pack files have random names and sizes generally unrelated to content due to splitting and merging (Source), exposing only p versus q versus x prefixes for data, metadata and indices (Source).
Active-server protection is where versiond needs append-only manifests plus local verification. Borg builds a Horton DAG where every object is referenced by its parent via plaintext-MAC ID up to a TAM-signed manifest protecting the fixed 000…000 manifest ID with HKDF-derived TAM keys (Source), tracks nonces to catch replay, and ships append-only mode that never overwrites committed segments with transaction-log rollback (Source), yet even Borg suffered CVE-2023-36811 allowing faked archives with repo write (Source). Restic authenticates objects but concedes it is not designed to protect against attackers deleting files and not designed to detect timestamp-grouped deletion (Source), warns that write access lets an attacker create garbage snapshots covering modified files and wait for forget to prune correct ones (Source), and leaves management-key separation as open issue discussion (Source). Kopia relies on snapshot verify walks plus sampled content downloads during maintenance (Source) and pushes ransomware safety to provider restricted keys plus COMPLIANCE object-lock with retention periods (Source), which on WebDAV has no equivalent since object-lock currently only supports S3 repos (Source). For versiond on dumb WebDAV the synthesis is concrete: pack spool files into fixed-size-class encrypted blobs with random names, pad small manifests, jitter upload times, never trust server mtime, verify after write with GET plus AEAD open before deleting staging, keep an encrypted local cache to prevent metadata leaks as restic does (Source), sign each append-only manifest with the master key and chain it to the prior manifest hash, enforce a local monotonic epoch to detect rollback, and compensate for missing server append-only with WebDAV versioning or a second prune identity that alone may delete, mirroring the split Borg recommends for serve keys.
Conclusion
The durable lesson is that cipher negotiation matters less than pipeline order, identifier keying, and operational separation of sync versus prune credentials. A daemon that hashes with a keyed MAC, compresses per chunk with an explicitly recorded codec, seals with a standard AEAD under HKDF-separated keys, and appends chained manifests into randomly sized packs inherits the best of Borg's authentication thinking and Kopia's WebDAV-ready packing without repeating restic's bespoke crypto or Duplicity's GnuPG fragility. What remains honestly unsolved is metadata-minimal versioning on a store that willingly reveals sizes and timestamps; the next hardening step is not a stronger cipher but deterministic padding classes, falsified access ordering, and provider-enforced immutability that make deletion the only move left to an untrusted host.