26 KiB
Backup encryption security choices
Which cipher and mode does each use? How are nonces/IVs generated and stored?
Takeaway
Restic uses non-standard AES-256-CTR + Poly1305-AES (Encrypt-then-MAC) with random 16-byte IV per file/blob; Borg 1.x uses AES-256-CTR + HMAC-SHA256 or keyed BLAKE2b-256 with 64-bit counters tracked by reservation, while Borg 2 moves to AEAD AES256-OCB and ChaCha20-Poly1305; Kopia uses AES256-GCM-HMAC-SHA256 by default (or CHACHA20-POLY1305-HMAC-SHA256) with per-content keys and random IVs; Duplicity delegates entirely to external GnuPG/OpenPGP (symmetric passphrase or public-key session-key hybrid).
Cited Findings
- Restic: “All data stored by restic in the repository is encrypted with AES-256 in counter mode and authenticated using Poly1305-AES” - Source
- Restic: “For encrypting new data first 16 bytes are read from a cryptographically secure pseudo-random number generator as a random nonce. This is used both as the IV for counter mode and the nonce for Poly1305” - Source
- Restic: format is “IV || CIPHERTEXT || MAC”, “complete encryption overhead is 32 bytes. For each file, a new random IV is selected”, 16-byte IV stored first, 16-byte MAC last - Source
- Restic: needs three keys: “a 32-byte key for AES-256 encryption, a 16-byte AES key and a 16-byte key for Poly1305”, the last 32 bytes split into 16-byte AES key
k+ 16-byterthen masked for Poly1305 per Bernstein paper - Source - Restic: pack files contain multiple independently encrypted/authenticated blobs plus encrypted header + 4-byte little-endian header length; blob types 0b00 data, 0b01 tree, 0b10/0b11 compressed data/tree (repo format v2, zstandard) - Source
- Restic: passwords wrapped via scrypt KDF (
N,r,p,salt); exampleN=65536, r=8, p=1, also observedN=32768, r=8, p=5in 2026 docs; derived 64 bytes split into 32-byte AES key + 32-byte MAC key; multiple key files per repo allow password change without re-encrypting data - Source - Restic current as of 2026 is v0.19.1 (5 Jul 2026) / v0.19.0, repo format version 1 or 2, v2 adds compression - Source
- Borg 1.x: “repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for authentication in an encrypt-then-MAC (EtM) construction” - Source
- Borg 1.x blake2 variants: “repokey-blake2 and keyfile-blake2 are also authenticated encryption modes, but use BLAKE2b-256 instead of HMAC-SHA256”, chunk ID is keyed BLAKE2b-256 - Source
- Borg chunk header: “TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT. Encryption and HMAC use two different keys” - Source
- Borg CTR IV: “A 64bit initialization vector is used”, “only 8 bytes of the 16 bytes nonce is saved in the payload, the first 8 bytes are always zeros”, limits capacity to 2**64 * 16 bytes (~295 exabytes) - Source
- Borg IV uniqueness via reservation: “initializes the encryption counter to be higher than any previously used counter value”, commits reservation by “taking the current counter value and adding 4 GiB / 16 bytes to the counter”, persisted via SaveFile to security DB + repository before encrypting - Source
- Borg pseudocode:
iv = reserve_iv(),encrypted = AES-256-CTR(enc_key, 8-null-bytes || iv, compressed),authenticated = type-byte || AUTHENTICATOR(enc_hmac_key, encrypted) || iv || encrypted- Source - Borg offline key wrapping: “256 bit key encryption key (KEK) is derived from the passphrase using PBKDF2-HMAC-SHA256 with a random 256 bit salt”, then Encrypt-and-MAC with “AES-256-CTR with a constant initialization vector of 0”, base64 keyblob in keyfile or repo config - Source
- Borg stable in 2026 is 1.4.5; crypto section still states “actual encryption is currently always AES-256 in CTR mode” for 1.x line - Source
- Borg 2 (beta 2.0.0b22/b23 as of 2026): modes
aes256-ocb,chacha20-poly1305,authenticated-sha256/blake3,none-sha256/blake3; “AES256 in OCB mode (encryption + authentication)” and “ChaCha20 + Poly1305” - Source - Borg 2: “All data can be protected client-side using 256-bit authenticated encryption (AES-OCB or chacha20-poly1305)” - Source
- Borg 2 goals: “get rid of AES-CTR mode and use ‘session keys’”, “use more modern / faster AEAD ciphers: AES-OCB and chacha20-poly1305”, “use a more modern KDF: argon2” - Source
- Kopia default:
const DefaultAlgorithm = "AES256-GCM-HMAC-SHA256"- Source - Kopia options: “By default, Kopia uses the AES256-GCM-HMAC-SHA256 encryption algorithm … but you can choose CHACHA20-POLY1305-HMAC-SHA256”, immutable after repo creation, selected via
--encryption=or Advanced Options - Source - Kopia per-content keys: registered as “AES-256-GCM using per-content key generated using HMAC-SHA256”, derives key via
deriveKey(p, purposeEncryptionKey)+hmac.New(sha256.New, keyDerivationSecret)pool, overhead 28 bytes - Source - Kopia format blob:
encryptionfield (defaultAES256_GCM),encryptedBlockFormat= JSONEncryptedRepositoryConfigencrypted with random IV prepended, keyKe = HKDF(SHA256, Km, UniqueID, "AES", 32)whereKm = PBKDF(passphrase, UniqueID)via scryptN=65536, r=8, p=1, AD =HKDF(SHA256, Km, UniqueID, "CHECKSUM", 32)- Source - Kopia
ContentFormatholdsHash,Encryption,HMACSecret,MasterKey (SIV-mode only),MaxPackSize; repository config stored encrypted because it contains key material - Source - Kopia observed version v0.23.1 in Go docs (2026 architecture doc updated Feb 2026) - Source
- Duplicity: “incrementally backs up files and folders into tar-format volumes encrypted with GnuPG” - Source
- Duplicity key modes:
--encrypt-key key(public-key, repeatable),--hidden-encrypt-keyusesgpg --hidden-recipientto hide recipient key ID,--sign-key, symmetric viaPASSPHRASEenv,--gpg-options,--gpg-binary- Source - Duplicity relies on OpenPGP hybrid: random session key
s,Enc_ri(s)per recipient +Enc_s(data)- Source - Duplicity current lineage includes 2.x/3.x (Debian bookworm-backports, Arch manuals); backend still GnuPG + librsync incremental - Source
Inferences
- Restic’s construction is bespoke AES-CTR + Poly1305-AES (not AES-GCM or ChaCha20-Poly1305); random IV per encryption avoids counter-reuse tracking at cost of 16-byte IV storage and reliance on CSPRNG.
- Borg 1.x’s 8-byte stored counter is the scaling bottleneck that motivated Borg 2 session keys + AEAD nonces; Borg 2 OCB vs ChaCha choice is performance-driven (hardware AES vs portable).
- Kopia’s “-HMAC-SHA256” suffix denotes per-content key derivation + outer HMAC layer, not just plain GCM; format-blob AD binding to passphrase-derived Km prevents config swapping without password.
- Duplicity inherits whatever GnuPG/OpenPGP negotiates (modern GnuPG 2.x defaults to AES-256 + MDC/AEAD), so its cipher agility is a GnuPG property, not a Duplicity design choice.
Gaps
- Exact GnuPG symmetric cipher/MDC vs AEAD (OCB/EAX) negotiated by Duplicity 3.x in 2026 not confirmed from Duplicity docs; depends on installed GnuPG version and
--gpg-options. - Kopia nonce length/placement for content blobs (vs format blob) not fully detailed in fetched docs; source states overhead 28 bytes but byte layout not quoted.
- Borg 2 session-key derivation and nonce construction details not yet in stable internals doc (beta docs only list mode names).
What is encrypted vs plaintext (content, filenames, metadata, manifests/snapshots)?
Takeaway
Restic, Borg (in encrypted modes) and Kopia encrypt file contents, filenames/paths, metadata and snapshots/manifests client-side; only key-file wrappers, config envelopes, outer pack/segment sizes, timestamps and directory layout remain visible. Duplicity encrypts tar volumes including embedded filenames + manifest/sigtar, but volume filenames, sizes and increment metadata stay plaintext. None hide sizes, counts or access patterns.
Cited Findings
- Restic guarantee: “Unencrypted content of stored files and metadata cannot be accessed without a password … Everything except the metadata included for informational purposes in the key files is encrypted and authenticated. The cache is also encrypted to prevent metadata leaks” - Source
- Restic snapshots are “JSON document … stored in a file below snapshots”, encrypted as
IV || Ciphertext || MAC(v1 JSON, v2encoding_version || zstd(JSON)); example snapshot containspaths, hostname, username, uid/gid, tags, treeonly after decryption - Source - Restic trees contain
name, type, mode, mtime/atime/ctime, uid/gid/user, inode, size, content[plaintext hashes], subtree, linktargetencrypted inside tree blobs - Source - Restic index JSON (
packs[].blobs[].id/type/offset/length) and pack headers (plaintext hashes, offsets) are encrypted; only after decryption are blob IDs visible - Source - Restic config (
version, id, chunker_polynomial) is encrypted;keys/files expose only informationalhostname, username, created, kdf params, salt+ encrypteddatawrapping master keys - Source - Borg attack model: client trusted, repository/server untrusted with full read/write/MITM; guarantees attacker cannot modify data, rename/remove/add archive, recover plaintext, or recover definite structural info (object graph) undetected - Source
- Borg: “authenticated encryption technique makes it suitable for backups to targets not fully trusted” - Source
- Borg authenticated-only modes (
authenticated-sha256/blake3) provide tamper detection without confidentiality;nonemodes provide neither - Source - Borg compression happens before encryption:
compressed = compress(data)then encrypt-then-MAC; optionalobfuscatepseudo-compressor pads with 0x00 before encryption to hide compressed sizes - Source - Kopia: “Encryption is at the repository level, and Kopia encrypts all snapshots in all repositories by default” via repository password - Source
- Kopia layers: BLOB store holds packs; CABS encrypts blocks after hashing (
AES256-GCM-HMAC-SHA256orCHACHA20-POLY1305-HMAC-SHA256); CAOS directory listings (kprefix), manifests (m), indirect JSON (x) are CABS blocks; LAMS manifests (snapshots, policies) stored as CABS blocks - Source - Kopia: “Pack files in blob storage have random names and don’t reveal anything about their contents or structure. Their sizes are also generally unrelated to their content due to splitting and merging” - Source
- Kopia blob prefixes
p(data packs),q(metadata packs),x(indices), object prefixesk/m/x,Ivirtual indirection - visible types but not contents - Source - Duplicity volumes are gzipped tar + GnuPG; OpenPGP literal-data packet (filename, mode, mtime) is inside encryption, so embedded names hidden, but outer
duplicity-full|incvolume filenames,manifest.gpg/sigtar.gpgnames, sizes and S3 storage-class split (manifest/sigtar on Standard for quick retrieval, data on Glacier/Deep Archive) are server-visible - Source - Duplicity over S3 with
--s3-use-glaciernotes: “Duplicity will store the manifest.gpg and sigtar.gpg files … on AWS S3 standard storage … all other data is stored in S3 Glacier” - Source - Duplicity
--no-encryptionwrites only gzipped volumes (no confidentiality) - Source
Inferences
- For Restic/Borg/Kopia, filenames, symlink targets, ownership, timestamps and snapshot manifests are as confidential as file bytes; server compromise reveals at most sizes/counts/timing, not names.
- Kopia’s p/q/x prefix separation intentionally leaks data-vs-metadata partitioning to aid storage management while hiding actual object graphs.
- Duplicity’s S3 Glacier optimization trades metadata confidentiality (manifests pinned to Standard, easily listable) for cheaper restores.
Gaps
- Whether Borg archive names themselves (manifest entries) are fully encrypted vs length-leaking was not explicitly quoted; inferred encrypted from manifest DAG but needs source confirmation.
- Kopia policy/manifest label (key=value) visibility to server (for server-managed repos) not clarified in fetched docs.
- Duplicity filename encryption inside OpenPGP (literal packet) assumed from GnuPG behavior, not Duplicity doc; no Duplicity doc explicitly states embedded names are hidden.
How are object names derived (plaintext hash, HMAC, random IDs) and what does that leak to an untrusted server?
Takeaway
Restic outer filenames are SHA-256 of ciphertext (verifiable via sha256sum) while inner blob references are SHA-256 of plaintext hidden in encrypted index/headers; Borg chunk IDs are HMAC/keyed hashes of plaintext (dedup without revealing content); Kopia content IDs are hashes/HMACs of plaintext with secret, packed into randomly named blobs; Duplicity names are sequential timestamps leaking backup chain. All leak dedup equality, counts, sizes and access patterns to varying degrees.
Cited Findings
- Restic: “storage ID is the SHA-256 hash of the content of a file”, filename is “lower case hexadecimal representation of the storage ID”, verifiable by running
sha256sumon file and comparing to filename - Source - Restic: “All content … is referenced according to its SHA-256 hash”, file split into CDC blobs, “SHA-256 hashes of all Blobs are saved in ordered list”, tree
contentholds plaintext hashes,subtreeholds plaintext tree ID - Source - Restic: index/pack header “yields all plaintext hashes, types, offsets and lengths”, pack
idin index is outer pack hash;restic cat packverifies hash and warns on mismatch - Source - Restic data/keys layout:
data/XX/<full-hash>,snapshots/<id>,index/<id>,keys/<id>,locks/,tmp/; snapshot filename is storage ID - Source - Restic attacker with read access can “Infer which packs probably contain trees via file access patterns”, “Infer the size of backups by using creation timestamps”, and pre-0.18.0 could derive chunker polynomial from observed chunk sizes per 2025 IACR paper; 0.18.0 mitigates by “randomly assigning chunks to pack files” - Source
- Borg: “key = id = id_hash(unencrypted_data)”,
id_hashissha256(no keys) orhmac-sha256(with keys); must be cryptographically strong for dedup - Source - Borg 1.x: chunk ID
id = AUTHENTICATOR(id_key, data)with independentid_keyvsenc_hmac_key; decryption assertsCONSTANT-TIME-COMPARISON(chunk-id, AUTHENTICATOR(id_key, decompressed))- Source - Borg 2: “A chunk is considered duplicate if its id_hash value is identical”,
id_hashe.g. “(hmac-)sha256 or (keyed) blake3”, selectable via--id-hash=blake3- Source - Borg manifest has fixed ID
000…000, anchored via TAM:tam_key = HKDF-SHA-512(ikm=id_key||enc_key||enc_hmac_key, salt, "borg-metadata-authentication-manifest"),HMAC(tam_key, packed)- Source - Borg fingerprinting: “repository does not hide size of chunks”, buzhash chunking uses “secret, random per-repo chunker seed”, small files <512 KiB yield single chunk; attacker with candidate files could brute-force fingerprint by sizes; mitigations: chunker choice/params, secret seed, compression choice,
obfuscatepadding; proximity/order (inode-order scan, segment adjacency) may leak additional info - Source - Kopia: “Block ID is generated by applying cryptographic hash function such as SHA2 or BLAKE2S”, identical blocks yield identical IDs for natural dedup; after hashing, block encrypted; multiple blocks merged into 20-40MB packs; index maps block ID -> (blob name, offset, length) - Source
- Kopia: pack blob names random (e.g.
pb4cf8…,q7a99…,xn0_20db…); content list/show viakopia content list/showrequires decryption - Source - Kopia format
UniqueIDis 32 random bytes, also PBKDF salt and HKDF info, preventing cross-repo ID correlation - Source - Duplicity sets: “files in full backup sets will start with duplicity-full while the incremental sets start with duplicity-inc”, ordered patches; deleting full invalidates dependent incrementals - Source
- Duplicity S3 observer can tell “that you are using Duplicity, the name of the bucket, your AWS Access Key ID, the increment dates and the amount of data in each increment” (affects connection, not GPG payload) - Source
- Duplicity default recipient key IDs visible in OpenPGP packets unless
--hidden-encrypt-key(--hidden-recipient) used; restore then tries all secret keys - Source
Inferences
- Restic outer names (ciphertext hashes) look random to server; inner plaintext hashes never appear plaintext on server, so equality of files is hidden unless attacker correlates sizes/access patterns - unlike Borg/Kopia where HMAC IDs are server-visible keys enabling dedup-equality oracle.
- Borg/Kopia HMAC-ID design intentionally reveals equality (required for server-side dedup) but not content; without
id_key/HMACSecretserver cannot confirm guesses except via size/proximity fingerprinting. - Duplicity leaks the most metadata: full vs incremental, chain order, timestamps and volume counts directly encode backup history even when payloads are opaque.
Gaps
- No reliable source confirming whether Restic pack filename is hash of ciphertext vs plaintext; doc says hash of content but pack verification suggests ciphertext - ambiguity noted.
- Kopia whether content IDs are plain hash vs HMAC-SHA256 with
HMACSecretnot fully resolved: architecture says hash,ContentFormat.HMACSecretimplies keyed; exact construction needs source read. - Borg 2 object store layout (keys/ namespace, segment naming) not fetched; assumed similar to 1.x but with AEAD.
Any documented rationale for their choices (audit reports, design docs)?
Takeaway
Restic cites simplicity + Encrypt-then-MAC robustness and documents threat model + 2025 chunking-attack mitigation; Borg cites Encrypt-then-MAC robustness, Horton principle + TAM (CVE-2016-10099), and Borg 2 session-key/AEAD/Argon2 motivations; Kopia cites envelope encryption + per-content keys + random packing; Duplicity cites delegation to GnuPG/librsync. No Cure53-style audit report was found for these four in the searches; strongest external reviews found are Filippo Valsorda’s Restic note and Borg issue discussions.
Cited Findings
- Restic design: “Encryption is a first-class feature, the implementation looks sane and I guess the deduplication trade-off is worth it” - Filippo Valsorda, quoted in Restic encryption docs - Source
- Restic threat model: trusted client + authentic restic + secret password; guarantees vs assumptions listed; “Advances … against … (AES-256-CTR-Poly1305-AES and SHA-256) have not occurred”, brute-force infeasible, leaked key requires full re-encryption via
copy/new repo - Source - Restic 2025 chunking attack: paper “Chunking Attacks on File Backup Services using Content-Defined Chunking” by Alexeev/Percival/Zhang, mitigated in 0.18.0 by random pack assignment; random irreducible CDC polynomial stored in encrypted
configto harden watermark attacks - Source - Restic CDC: Rabin fingerprints, 64-byte window, files <512 KiB unsplit, 512 KiB–8 MiB blobs targeting 1 MiB average - Source
- Borg: “Encryption is currently based on the Encrypt-then-MAC construction, which is generally seen as the most robust way” - Source
- Borg: follows “Horton principle … not only the message must be authenticated, but also its meaning”, object ID MACs plaintext, parent reference assigns meaning, DAG anchored by TAM - Source
- Borg TAM added since 1.0.9 for “Pre-1.0.9 manifest spoofing vulnerability (CVE-2016-10099)” - Source
- Borg: “Borg does not support unauthenticated encryption - only authenticated encryption … No unauthenticated schemes will be added” - Source
- Borg multi-client caveat: with multiple independent clients on same repo, “Borg fails to provide confidentiality” due to counter-reservation replay; trusted sync channel needed - Source
- Borg compression+encryption discussion in issue #1040 concluded “no problem at all” to “hard and extremely slow to exploit”; user can disable compression - Source
- Borg primitives rationale: HMAC-SHA256 vs BLAKE2b chosen on SHA hardware support (Ryzen, Intel 10th+ mobile/11th+ desktop, M1+, ARM64 SHA ext favour HMAC-SHA256; 64-bit CPUs without SHA ext favour BLAKE2b); uses OpenSSL libcrypto only, not libssl/TLS/X.509 - Source
- Borg 2 rationale: global AES+MAC key requires perfect counter tracking across clients/threads (XOR-plaintext leak risk + sync complexity); session keys ease multithreading - Source
- Borg keyed chunkers (
toeplitz-aes/rabin-aes/goldilocks-aes) rationale: “make chunk-boundary fingerprinting attacks much harder” - Source - Kopia rationale: “standard envelope encryption technique to de-couple the repository passphrase from keys used for encrypting/authenticating contents”; format blob holds params,
UniqueIDrandom per repo, scrypt PBKDF + HKDF-SHA256 for Ke/AD - Source - Kopia FAQ: encryption mandatory, algorithm fixed at creation, password unrecoverable - Source
- Duplicity rationale: uses librsync for space-efficient incrementals + GnuPG so “they will be safe from spying and/or modification by the server” - Source
- Duplicity signing+symmetric-encrypt via CLI gpg noted as “specifically challenging”, only certain passphrase/agent combos tested working - Source
Inferences
- Restic/Borg explicitly prefer Encrypt-then-MAC over AEAD for 1.x-era compatibility and library constraints (OpenSSL libcrypto, Go stdlib); Borg 2 and Kopia converge on modern AEAD (OCB/GCM/ChaCha20-Poly1305) once widely available.
- Borg’s extensive fingerprinting/TAM documentation is the most explicit untrusted-server rationale among the four; Restic’s 2025 mitigation shows similar concerns emerging later.
- Duplicity offloads rationale to GnuPG/OpenPGP; its docs contain no cipher-selection rationale beyond backend/storage-class notes.
Gaps
- No Cure53 or similar third-party audit report for restic/Borg/Kopia/Duplicity found in searches (results returned unrelated Ente/Project11/Monocypher audits); if audits exist, they were not retrievable with queries used.
- Kopia independent security review or design-rationale doc beyond envelope-encryption page not found.
- Duplicity successor scope (e.g. duplicacy, duplicity 3.x forks) not clarified; findings cover classic Duplicity only.