brb

Independent, mountable, encrypted backup discs.

brb bin-packs a directory tree into disc-sized groups, builds one self-contained SquashFS image per disc, encrypts it with age, computes par2 recovery data over the ciphertext, and images each disc to an ISO ready to burn to BD-R, BD-R DL or BDXL.

The last step of a restore is
mount -o loop,ro disc07.squashfs /mnt

which needs nothing but the Linux kernel — no brb, no Python, no bespoke archive format.

Install Source v1.0.0 · MIT
brb 1.0.0
$ brb plan
   . config: /home/jz/.config/brb/config
==> scanning /home/jz
  ok 431006 entries, 198.42 GiB of file data before compression
==> planning disc layout (dry run, nothing is built)
   . raw content budget per disc: 20.49 GiB  (image budget 20.49 GiB / ratio 1.000)
   . disc 01: 118204 files, 20.49 GiB raw
   . disc 02:  47455 files, 20.48 GiB raw
   . disc 03:  91620 files, 20.49 GiB raw
   . disc 04:  62933 files, 20.47 GiB raw
   . disc 05:  38017 files, 20.49 GiB raw
   . disc 06:  21744 files, 20.48 GiB raw
   . disc 07:  15108 files, 20.49 GiB raw
   . disc 08:   9622 files, 20.46 GiB raw
   . disc 09:   5904 files, 20.49 GiB raw
   . disc 10:   2232 files, 14.08 GiB raw
  ok 10 disc(s) at pack ratio 1.000, 198.42 GiB of raw content
$ brb backup --verify-roundtrip
warn staging holds UNENCRYPTED squashfs images while the backup runs
   . each image is deleted as soon as it is encrypted and verified
Continue? [y/N] y
==> building images (image budget 20.49 GiB per disc)
   . disc 1: packing 118204 files, 20.49 GiB raw
  ok disc 1 image: 17.24 GiB (compressed to 0.841 of raw)
   . encrypting and hashing disc01.squashfs
   . encrypt disc01.squashfs [========================] 100.0%  17.24 GiB / 17.24 GiB
   . generating 10% recovery data (17652 blocks of 1.00 MiB, 1765 recovery blocks)
   . verifying disc 1 decrypts back to the same bytes
  ok disc 1 round-trip verified
  ok disc01.squashfs encrypted, protected, plaintext removed
  ok backup complete in 214 minute(s)

01 — the whole idea

Every disc is independent.

Losing disc 7 loses exactly the files on disc 7. Every other disc still restores on its own. There is no parity spread across the set, no volume 1 of 20 that the rest depend on, no catalogue you have to find first. Each disc is a complete, mountable filesystem.

Ten discs, 412,839 files, 198.42 GiB. Each one restores on its own.

set 10 of 10 discs restore on their own
files 412,839 of 412,839 still restore
data 198.42 GiB of 198.42 GiB still restore

Nothing is damaged. Any one of these discs restores its own files without the other nine.

A scratch is not a loss

par2 recovery data is computed over the encrypted bytes, at 10% by default, so it protects exactly what is physically on the disc. A disc that no longer reads cleanly is copied off, repaired from the par2 volumes stored beside it, then decrypted as normal. Nothing is written back to the disc — a BD-R is written once.

A loss is bounded

When par2 cannot repair an image, brb refuses to decrypt it rather than hand you plausible garbage. You lose that disc's files and nothing else — and the encrypted index on every other disc still tells you exactly which files those were.

Every disc carries the shape

Each disc carries the full directory skeleton of the original tree — directories, symlinks, device nodes. Mount any single disc and you see the whole shape of the backup, with that disc's files present and the rest absent.

02 — how a disc is made

One direction, and nothing invented at the end of it.

The bin-packer is brb's own. Everything it hands off to is a format that was specified, documented and widely implemented long before brb, and that has every chance of outlasting it.

  1. 1 bin-pack the tree into disc-sized groups

    Files are sorted largest-first and packed greedily into one open disc at a time, backfilling smaller files into whatever budget is left. Hard-link groups are one indivisible unit, charged once. A single file larger than one disc is detected and refused rather than silently dropped.

    Discs are packed by uncompressed size, so brb has to guess the compression ratio before it compresses. It re-learns that guess from every finished disc.

  2. 2 mksquashfs one self-contained image per disc

    The image is a plain SquashFS 4.0 filesystem — zstd, 1 MiB blocks, xattrs preserved. The file list arrives on stdin as NUL-delimited paths, so no filename can be misparsed.

    mksquashfs - disc01.squashfs -cpiostyle0 -no-progress -quiet \
      -b 1M -xattrs -no-exports -comp zstd -Xcompression-level 19
  3. 3 age encrypt the image to public keys

    Encryption uses the age library in-process, not the age binary, so the format is identical and one fewer thing has to be installed. Both the plaintext and ciphertext SHA-512 are computed in the same pass. The ciphertext hash is the one that describes what is physically on the disc; the plaintext hash is what proves the decryption came back right.

    Backup needs public keys only, so no secret sits on the backup machine for the days a set takes to build. The one exception is --verify-roundtrip, which decrypts each image back and compares hashes before the plaintext is deleted: that needs the identity present for the run, and it is the only way to prove the set decrypts while you can still do something about it.

  4. 4 par2 recovery data over the ciphertext

    Parity is computed over the encrypted bytes — the ones that will actually rot — at 10% by default. The small files beside it (the hashes and the encrypted index) get their own par2 set at 50%, because they are tiny and losing them is annoying.

    par2 create -q -r10 -n1 -b17652 -- disc01.squashfs.age
  5. 5 xorriso one ISO per disc

    ISO 9660 level 3 with Rock Ridge and Joliet. Level 3 multi-extent is what allows image files above 4 GiB. Not UDF — xorriso cannot write it, so brb does not claim otherwise on the disc. Every Linux system reads ISO 9660 anyway; some appliances that expect UDF on Blu-ray may not.

    xorriso -as mkisofs -quiet -iso-level 3 -r -J -joliet-long \
      -V BACKUP_01_OF_10 -A "brb 1.0.0" -o disc01.iso discs/disc01
  6. 6 BD-R write it, then read it back

    ISOs are built as each disc goes into the drive and removed once it is written, so staging stays near the size of the compressed set rather than roughly 2.2× that size for the length of a burn campaign. verify-disc then reads the burned disc back and checks every hash.

03 — what you can hold

What ends up on each disc.

Nothing on a disc is a private format. The image is a filesystem, the hashes are GNU sha512sum files, the index is a two-column TSV, and the recovery data is par2. All of it is legible to tools that predate brb by decades.

README.md                       restore instructions, written for a stranger
MANIFEST.txt                    the whole set, and the exact tool versions used
SHA512SUMS                      hashes of every file on this disc
brb.sh                          the bash reader — about a thousand lines
brb-linux-amd64                 static Go binary, 64-bit Intel/AMD
brb-linux-aarch64               static Go binary, 64-bit ARM
brb-src.tar.gz                  complete source, dependencies vendored
data/
  disc01.squashfs.age           the filesystem image, encrypted
  disc01.squashfs.age.sha512    hash of the encrypted image
  disc01.squashfs.sha512        hash of the image AFTER decryption
  disc01.squashfs.age.par2      par2 index
  disc01.squashfs.age.vol*.par2 recovery data, 10%
  index.tsv.gz.age              which disc holds which file, encrypted
  index.tsv.gz.age.sha512       hash of the encrypted index
  sidecars.par2                 par2 index for the small files
  sidecars.vol*.par2            recovery data for them, 50%

04 — the point of the design

Restoring without brb.

A backup format that needs its own software to read is a bet that the software survives. This one does not take that bet. Given one disc and your age identity, the whole restore path is four standard commands:

  • cpcoreutils
  • sha512sumcoreutils
  • par2only if it rotted
  • ageone static binary
  • mountthe kernel
  1. cp /mnt/data/disc07.squashfs.age .

    Take the image off the disc. If it will not read cleanly, use ddrescue — it fills unreadable regions with zeros and keeps going, which is exactly what par2 needs.

  2. sha512sum -c disc07.squashfs.age.sha512 || par2 repair -- disc07.squashfs.age.par2

    Check it, and repair it if the disc has rotted.

  3. age -d -i identity.txt -o disc07.squashfs disc07.squashfs.age

    Decrypt it. A passphrase-protected identity works anywhere a plain one does.

  4. mount -o loop,ro disc07.squashfs /mnt

    Mount it. This step needs only the kernel. unsquashfs -d /dest disc07.squashfs extracts instead, if you would rather have the files than the mount.

Two programs, one format

brb ships as two implementations with a deliberate split. The Go build is the tool you use day to day. The bash script exists for one reason: a restore fifteen years from now, by someone holding a disc and no particular reason to trust a static binary.

Implementation Can Commands
Go
brb-linux-amd64
writes and reads doctor, init-key, plan, backup, burn, iso, verify-disc, ingest, restore, mount, list, index
bash
brb.sh
reads only doctor, verify-disc, ingest, restore, mount, list, index

brb.sh refuses backup, plan, burn, iso and init-key by name and says where they went, rather than failing as an unknown command.

Held to the same format

A cross-compatibility suite builds a set with the Go build and reads it with both, then asserts they produce identical answers — down to the encrypted index and the byte-for-byte restored tree. It also runs the recipe printed on the disc itself, with neither implementation involved.

Then it damages a set on purpose. A rotted .sha512 sidecar must not condemn an image par2 proves is whole; an image par2 cannot repair must still be refused rather than decrypted.

Where the two genuinely differ, the check is written the way it ought to pass and marked XFAIL with the divergence named. An XFAIL that starts passing is reported as XPASS and counted as a failure, so a fixed divergence gets promoted to a real assertion instead of sitting in the ledger forever.

05 — keys

Public keys go in. Nothing secret has to.

Every line of the recipients file is an age recipient, every image is encrypted to all of them, and any single matching identity restores the whole set on its own. Compromising the backup machine yields nothing that outlives the compromise — unless you are running --verify-roundtrip, which needs the identity there for the length of the run.

The price of that design is a single point of failure in the other direction: lose your identity file and the archive is gone, permanently and irrecoverably. So back it up somewhere that is not these discs — a password manager, and printed on paper. It is one short line.

You can also give yourself a second way in: a rescue key. Mint a second keypair whose private half never touches the disk in plaintext —

age-keygen | age -p -o ~/.config/brb/rescue-identity.txt.age

The identity goes from age-keygen's stdout straight into age -p's stdin. It is never a file, so there is nothing to shred afterwards — which matters, because shred cannot promise anything on a copy-on-write, compressed or flash-translated filesystem. What lands on disk is a small file encrypted under a passphrase you chose, and it is inert without that passphrase.

age-keygen prints the matching public key to the terminal on stderr, so the pipe does not swallow it. That line is the half you have to keep, and appending it is a separate step — nothing above did it for you:

echo 'age1...' >> ~/.config/brb/recipients.txt

Every disc built from that point on is encrypted to both keys, and either one restores the whole set. Discs already burned are not — they were encrypted to the recipients that existed when they were written.

The corollary: never put that file on these discs. A file stored next to its own passphrase, or on the media it unlocks, is one secret, not two.

Why not just a passphrase?

The obvious request is "let me use a passphrase instead of a key file". brb deliberately does not allow it, for three reasons.

  1. age will not express "key OR passphrase" in one file

    A passphrase in age is an scrypt stanza, and age refuses to mix an scrypt stanza with recipient stanzas. Encrypting with a passphrase means giving up recipients entirely — no multi-key sets, no rescue key, no third party who can restore for your estate.

  2. A stolen disc is ciphertext forever

    Whoever holds the disc holds the attack surface, on their hardware, with unlimited time. A passphrase a human can memorise loses that race over the decades this medium is meant to last. An X25519 key does not.

  3. Backup would need a secret on the machine

    Today it needs only public keys, and the one flag that wants the identity is opt-in. Passphrase encryption would make the ability to decrypt every disc a permanent fixture of the backup host.

06 — media

Four disc geometries.

The image budget is what is left after reserving 2% for ISO 9660 overhead, 100 MiB for the plaintext files carried on every disc, and room for the par2 recovery data. Raising redundancy takes its share out of the same budget.

DISC_TYPE Media Raw capacity Usable image budget
bd25 BD-R single layer 23.31 GiB 20.49 GiB
bd50 BD-R DL dual layer 46.61 GiB 41.07 GiB
bdxl100 BDXL triple layer 93.23 GiB 82.22 GiB
bdxl128 BDXL quad layer 119.21 GiB 105.16 GiB

Budgets assume the default PAR2_REDUNDANCY=10. A twenty-disc set takes days to build, so backup records its progress after every completed disc: an interruption — a reboot, a full disk, a Ctrl-C — costs only the disc that was in flight, and brb backup --resume picks up at the next one.

The disc is the one variable brb does not control

brb writes a standard ISO and hands it to your burner. What that lands on is your choice, and it is the only part of this that a format cannot help with — no amount of par2 saves a disc whose recording layer has stopped holding a signal.

Blu-ray starts from a better position than the media most people mean by "burning a backup". CD-R and DVD±R record into an organic dye, and dye is what fades. BD-R HTL — the ordinary kind — already uses an inorganic recording layer instead, so the classic failure of consumer optical media largely does not apply. BD-R LTH is the exception: it is dye-based, it is usually the cheaper shelf, and it is worth checking which one you are buying.

M-DISC

M-DISC BD-R is the archival grade of the same idea: an inorganic layer the laser physically etches rather than bleaches. It burns and reads in any Blu-ray drive, needs no flag and no different configuration, and nothing on this page changes if you use it. It is sold as 25, 50 and 100 GB, so bd25, bd50 and bdxl100 are the geometries available to you — bdxl128 is not.

The thousand-year figure on the packaging comes from accelerated-ageing extrapolation (ISO/IEC 16963), not from anyone having waited, and the honest gap between good HTL BD-R and M-DISC is narrower than the marketing suggests. It buys better odds at a higher price per gigabyte, which is a reasonable trade for a set you intend to shelve and forget. It does not buy a different kind of promise — which is why every disc still carries 10% par2, why the format is one the kernel can already mount, and why the only backup you know works is one you have restored.

07 — get it

Install, then prove it restores.

Build

The Go build is the one to install — it is the only one that can write a set. Go 1.25 or newer; a prebuilt static binary needs nothing at all.

cd go && go build -o ~/.local/bin/brb ./cmd/brb

The bash reader is a single self-contained script with nothing to build. Install it under a distinct name — both name themselves after however they were invoked, and one of them cannot back up.

install -Dm755 brb.sh ~/.local/bin/brb.sh

Dependencies

The distinction that matters is not between the two programs but between writing discs and reading them. Writing needs more tooling, and you need it today. Reading is what someone needs in fifteen years, and it is deliberately tiny.

sudo dnf install age squashfs-tools par2cmdline xorriso

On Debian and Ubuntu par2cmdline is par2 and ddrescue is gddrescue; on Arch xorriso comes from libisoburn. squashfs-tools must be 4.5 or newer — brb feeds mksquashfs a NUL-delimited file list via -cpiostyle0, which does not exist in 4.4. brb doctor tests for the flag directly rather than parsing a version string, so trust it over any table.

A typical run

# dependencies, budgets, and can you decrypt it
brb doctor
# then back the key up OFF these discs
brb init-key
# how many discs, before committing to anything
brb plan
brb backup --verify-roundtrip
brb burn all
brb verify-disc 1
# once, before you trust the set
brb restore /tmp/testrestore
# staging held plaintext; wipe it
rm -rf /var/tmp/brb

Do the restore at least once, before you trust the set with anything. A backup you have never restored is a hypothesis, not a backup.

Known limits

  • Not incremental. Every run is a full backup of the whole tree. A completed set cannot be updated in place.
  • Staging holds plaintext while a run is in flight. Each image is deleted as soon as it is encrypted, but one disc's worth is on disk at any moment. Put staging on an encrypted volume, or wipe it afterwards.
  • A single file larger than one disc cannot be stored. brb detects this during plan and backup, and stops rather than silently dropping it.
  • ISOs are ISO 9660 level 3, not UDF. Any Linux system reads these; some appliances that expect UDF on Blu-ray may not.