backupy-agent/apps/backupy-decrypt
TronoSfera 8b0c978337 feat(initial): Backupy agent + backupy-decrypt CLI
Source ports from the TronoSfera/backupy-cloud monorepo:
- apps/agent/        — Go agent (WSS client, persistent queue, Docker
                       discovery, 5 DB drivers: PG/MySQL/Mongo/Redis/SQLite,
                       pre/post hooks, Prometheus metrics)
- apps/backupy-decrypt/ — standalone CLI for client-side decryption
- packages/proto/    — protobuf wire format (generated .pb.go committed
                       so the repo builds without protoc)
- docs/              — agent spec + wire-protocol contract

Apache-2.0 license. Image published to ghcr.io/tronosfera/backupy-agent
on every v* tag via .github/workflows/release.yml (multi-arch amd64+arm64).
2026-05-17 20:22:35 +03:00
..
cmd/backupy-decrypt feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
internal feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
.gitkeep feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
.goreleaser.yaml feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
doc.go feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
go.mod feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
go.sum feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
integration_test.go feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
Makefile feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00
README.md feat(initial): Backupy agent + backupy-decrypt CLI 2026-05-17 20:22:35 +03:00

backupy-decrypt

Offline CLI to decrypt a Backupy backup file.

The Backupy server never sees your plaintext data — it stores only the AES-256-GCM ciphertext your agent uploaded. To recover a backup you:

  1. Generate a download URL from the Backupy dashboard (GET /v1/runs/:id/download-url).
  2. Generate a decryption token from the same dashboard (POST /v1/runs/:id/decryption-token). This token is a JWT that contains the plaintext data-encryption key (DEK) plus integrity metadata. It is valid for 15 minutes.
  3. Download the encrypted blob with curl (or any HTTP client).
  4. Run backupy-decrypt --token <jwt> --in <blob> --out <plaintext>.

The CLI is fully offline — it never contacts Backupy infrastructure.

Install

Download the release archive for your platform from the GitHub releases page, extract it, and put the backupy-decrypt binary somewhere on $PATH:

# macOS (arm64) — adjust for your platform
curl -L https://github.com/backupy/backupy/releases/latest/download/backupy-decrypt_Darwin_arm64.tar.gz \
  | tar -xz
sudo mv backupy-decrypt /usr/local/bin/

Or build from source:

git clone https://github.com/backupy/backupy.git
cd backupy/apps/backupy-decrypt
make build
./bin/backupy-decrypt --version

Usage

$ backupy-decrypt --help
Decrypt a Backupy backup file.

Usage:
  backupy-decrypt --token <jwt> --in <encrypted-file> --out <plaintext-file>

Flags:
  --token            Decryption token (JWT) from the Backupy server
  --token-file       Read token from a file (avoids leaking via `ps`)
  --in               Encrypted input file (.enc)
  --out              Output plaintext file
  --verify-sha256    Verify the input's SHA-256 matches the token's claim (default true)
  --skip-decompress  Don't decompress zstd after decryption (default false — the
                     agent always zstd-compresses, so leave this off unless your
                     pipeline used compression=none)
  --quiet            Suppress progress output
  --version          Show version

Full example

# 1) Download the ciphertext.
curl -o backup.enc "<presigned_url_from_dashboard>"

# 2) Save the JWT into a file so it doesn't leak via your shell history.
echo "<jwt_from_dashboard>" > /tmp/backup.token

# 3) Decrypt + decompress in one shot. Output is the raw pg_dump / mysqldump.
backupy-decrypt \
    --token-file /tmp/backup.token \
    --in backup.enc \
    --out backup.sql

# 4) Restore yourself. Backupy intentionally does not run this step —
#    you stay in control of where the data goes.
psql -d mydb < backup.sql

Encryption format (verbatim)

The CLI implements the inverse of apps/agent/internal/pipeline. All integers are big-endian.

chunk := uint32 ciphertext_len      // bytes that follow, EXCLUDING this u32
         12-byte random nonce       // unique per chunk
         ciphertext (≤ 1 MiB + 16-byte GCM tag)

EOF marker := uint32 0              // appended after the final chunk
  • Plaintext chunk size is 1 MiB. The final chunk may be shorter.
  • tag is the 16-byte AES-GCM authentication tag, appended by Go's cipher.AEAD.Seal.
  • The Additional Authenticated Data (AAD) is nil. Per-chunk reorder and truncation defence comes from the explicit uint32 size prefix plus the mandatory zero-length EOF marker: any reordered or truncated stream either trips a frame-boundary error or fails the missing-EOF check.
  • The DEK is exactly 32 bytes (AES-256).

This format is canonical — apps/agent/internal/pipeline/encrypt.go is the single source of truth, and the CLI is implemented to be byte-for- byte compatible. This is enough to reimplement the decrypt step in any language — see the package doc for apps/backupy-decrypt/internal/decrypt.

Troubleshooting

Symptom Cause / fix
error: decrypt: token expired (request a new one) The JWT lifetime is 15 min. Issue a new token from the dashboard.
error: decrypt: AES-GCM authentication failed Wrong DEK or corrupted input. Re-download the file and re-issue the token.
error: decrypt: ciphertext SHA-256 mismatch The file you downloaded is not the file the server recorded. Re-download — the URL may have expired.
error: decrypt: input file is truncated Download didn't complete. Retry with curl -C - ... or a fresh URL.
Plaintext looks like binary garbage after decompress Your job was set to compression=none. Re-run with --skip-decompress.

Security notes

  • The decryption token is as sensitive as your data — anyone with the JWT in their possession can decrypt the file. Treat it like a temporary password: do not paste it into chat, do not commit it.
  • The CLI never writes the DEK to disk; it lives only in process memory and is zeroized before exit.
  • For an extra layer of safety pass --token-file so the token does not appear in your shell history or in ps.

License

Same as the parent Backupy repository.