# Encryption

This document is the user-facing summary of which encrypted-archive
schemes peel can decrypt, which it refuses (and why), and the threat
model the implementation is engineered against. The implementation
plan lives in [`PLAN_archive_encryption.md`](PLAN_archive_encryption.md);
this file is the shipping-feature surface.

## Supported schemes

| Format | Scheme | KDF | Authenticated | Status |
|--------|--------|-----|---------------|--------|
| zip | WinZip-AES (AE-1 / AE-2; AES-128/192/256-CTR) | PBKDF2-HMAC-SHA1, 1000 iterations | HMAC-SHA1-80 trailer | shipping (§3) |
| zip | PKWARE traditional "ZipCrypto" (CRC32-keyed PRGA) | password-derived 12-byte header | none (CRC32 of plaintext) | shipping (§3b) — *insecure* |
| rar5 | AES-256-CBC, archive-header encryption (type 4) | PBKDF2-HMAC-SHA256 with `iterations = 1 << (kdf_count + 15)`; running-XOR fused stages produce AES key + HMAC key + pswcheck source in one pass | optional pswcheck (8-byte XOR-fold of `PBKDF2(c=N+2)` + 4-byte SHA-256 sum) | shipping (§4) |
| rar5 | AES-256-CBC, per-file encryption (extra record 1) | same as above (per-record salt / IV / `kdf_count`) | optional pswcheck (8-byte XOR-fold + 4-byte SHA-256 sum) | shipping (§4) |
| 7z | AES-256-CBC (coder id `06:F1:07:01`) | bespoke SHA-256 "round-tower" KDF (`crate::crypto::sevenz_kdf`) | none (CRC32 of plaintext) | shipping (§5) |

7z has no in-archive password verifier (unlike RAR5's optional
`pswcheck` or ZIP-AES's 2-byte PBKDF2 verifier); the only
correctness signal is the post-decryption CRC32 on each decoded
substream. A wrong password produces garbage plaintext, the CRC32
mismatches at the first substream boundary, and peel translates
that into [`EncryptionError::PasswordIncorrect`] — the user sees
the same exit code 4 and the same message shape as the other
formats.

### RAR5 encryption layers

RAR5 ships two independent encryption layers; an archive may use
either, both, or neither:

1. **Archive-header encryption** (HEAD_CRYPT, header type 4). When
   present, every header after HEAD_CRYPT is AES-256-CBC encrypted
   under a per-archive key derived from the password. Each
   encrypted header is prefixed by its own 16-byte IV and padded to a
   16-byte boundary on disk. Data areas are *not* encrypted by this
   layer; they pass through cleartext (or under per-file encryption,
   below). The walker in `src/download/rar_pipeline.rs` switches into
   encrypted-header mode after parsing HEAD_CRYPT and reads
   subsequent headers via `read_encrypted_header_window`.
2. **Per-file data encryption** (encryption record, extra-record
   type 1). Each file header may carry an encryption record with
   its own salt, IV, `kdf_count`, and optional pswcheck. When
   present, the file's data area is AES-256-CBC encrypted under a
   per-file key derived from the same password. The padding scheme
   matches archive-header encryption: zero-pad the plaintext to a
   16-byte boundary, encrypt, and report the on-disk size in the
   file header's `data_size` field. STORED entries discard the
   padding at the `unpacked_size` boundary; compressed entries
   discard it when the decoder reaches its natural EOF.

Both layers share a single password (resolved once per archive). When
a checkpoint resumes a partially-extracted run, encrypted entries
restart from byte 0 on the in-flight entry — the CBC chain cannot
yet be migrated across a checkpoint snapshot. The sink replays the
on-disk prefix to seed its hashes, so the user-visible bytes remain
byte-identical to a clean run.

### 7z encryption layer

7z has a single encryption shape: an AES-256-CBC coder (id
`06:F1:07:01`) that sits at the front of a folder's coder chain.
The coder's props blob encodes:

- `numCyclesPower` (low 6 bits of byte 0): the SHA-256 round-tower
  KDF runs `2^power` rounds.
- Optional salt (up to 16 bytes) and IV (up to 16 bytes), present
  when the high bits of byte 0 are set.

The KDF derives a 32-byte AES-256 key by hashing
`salt || password_utf16le || round_counter_le` for each of the
`2^power` rounds (`crate::crypto::sevenz_kdf::sevenz_derive_key`).
The on-disk IV is zero-padded to 16 bytes if shorter. The chain
then flows: ciphertext → AES-CBC decrypt → (optional LZMA2 / COPY)
→ folder substreams → per-file CRC32 → on-disk.

7z does not carry a password verifier. The first correctness
signal is the per-substream CRC32 inside the decoded plaintext;
under a wrong password the plaintext is random and the CRC32
mismatches with overwhelming probability. `FolderDecoder`
translates that into [`EncryptionError::PasswordIncorrect`]
when it knows the folder is encrypted (per the
`folder_is_encrypted` check). All folders in an archive share
one password (loaded lazily on the first encrypted folder),
matching 7-Zip's own behaviour. Resume from a checkpoint restarts
the in-flight folder from byte 0 — same constraint as RAR5's
per-file encryption, for the same reason (CBC chain state).

The reference for this implementation is the LZMA SDK
`CPP/7zip/Crypto/7zAes.cpp` source; the in-tree KDF
(`crate::crypto::sevenz_kdf`) and CBC primitives
(`crate::crypto::aes_modes::AesCbcDecrypt`) are mechanically
equivalent to the SDK's behaviour and have differential test
corpora against a reference `aes` / `cbc` crate.

## Supplying a password

Every encrypted path consumes a password via the
[`--password-from <SOURCE>`](../src/cli.rs) flag added in
[`PLAN_archive_encryption.md`](PLAN_archive_encryption.md) §1. Four
sources are supported:

- `prompt` — reads `/dev/tty` directly (never stdin, so a piped stdin
  carrying archive data cannot accidentally answer the prompt). Echo
  is disabled while reading. Up to 3 attempts on a wrong password
  before peel exits with code 4.
- `env:NAME` — reads the named environment variable. Strips a trailing
  newline; empty values are refused.
- `file:PATH` — reads the first line of `PATH`. peel emits a warning
  when the file's mode is not `0600`.
- `fd:N` — reads from file descriptor `N` (one-shot, until EOF or
  newline). Compatible with shell `< <(…)` redirections.

peel deliberately does **not** accept a `--password=<value>` flag.
Process-list visibility (`ps`, `/proc/<pid>/cmdline`) is the wrong
default for a passphrase; users who really want this can wrap with
`env:NAME PEEL_PW=… peel … --password-from env:PEEL_PW`.

Exit codes:

- `0` — extraction completed.
- `1` — generic extraction or I/O failure (anything not below).
- `4` — `EncryptionError::PasswordIncorrect` or
  `EncryptionError::PasswordMissing` anywhere in the error chain.
  Scripts that want to re-prompt distinguish password issues from
  archive corruption by checking this code.
- `128 + signum` — graceful shutdown after SIGINT (130) or SIGTERM
  (143). The `.peel.part` / `.peel.ckpt` sidecars are left on disk;
  re-running resumes.

## Threat model

peel **decrypts**. It does not authenticate the user; it has no
support for hardware tokens, smart cards, GPG-encrypted passphrases,
or biometric unlock. The user supplies a passphrase via one of the
[`--password-from`](#supplying-a-password) sources; everything beyond
that is the operating system's responsibility.

peel does **not** protect against an attacker with:

- read access to the process's address space (`/proc/<pid>/mem` on
  Linux, `vmmap` on macOS). The
  [`Password`](../src/secret.rs) wrapper zeroises its backing
  storage on drop via `ptr::write_volatile`, but a snapshot taken
  while the key derivation is in flight will see the cleartext.
- read access to the swap device. If the machine swaps mid-extraction
  the passphrase may be written to disk.
- read access to `argv` (which is precisely why `--password=<value>`
  does not exist).
- a precise micro-architectural timing side-channel (Spectre-class,
  cache-timing on a co-located VM). Tag comparisons go through
  [`crypto::ct_eq`](../src/crypto.rs) which is constant-time
  relative to the *length* of its inputs; that defends against the
  byte-walking class of timing attack but does not defend against
  attackers measuring cycle-level timing of the underlying AES /
  HMAC primitives.

peel **does** apply the following discipline:

- Every cryptographic primitive in [`src/crypto/`](../src/crypto.rs)
  is differentially cross-checked against a reference upstream crate
  pinned in `[dev-dependencies]` (`sha1`, `hmac`, `pbkdf2`, `sha2`,
  `aes`, `ctr`, `cbc`). The runtime binary does not link any of these
  crates; the reference implementations exist only so a reviewer can
  reproduce the corpus.
- Tag / verifier comparisons route through `crypto::ct_eq`. Password
  bytes never travel through any code path that prints `Debug`
  (the `Password` type explicitly redacts).
- All KDF iteration counts come from the archive's header; peel does
  not "guess" a sensible default. RAR5's `kdf_count` byte is capped
  at the spec maximum of 24 (= 2^39 iterations) before key
  derivation runs.

## Out of scope — permanently

- **Re-encrypting on the fly.** peel never encrypts. "Decrypt this
  remote archive and re-encrypt to a different password locally" is
  not a peel job; pipe to `7z` / `zip` if you need that.
- **Password-protected gzip / xz / lz4 / zstd.** None of these formats
  has a native encryption layer; the convention "GPG-encrypted
  tarball" is a separate pipeline.
- **ZIP central-directory encryption** (PKWARE strong-encryption
  spec, general-purpose flag bit 6). Used by approximately one
  product (SecureZIP) outside specific enterprise contexts. peel
  surfaces it as `ZipError::UnsupportedFeature { feature: "PKWARE
  strong encryption" }`.
- **Hardware-accelerated AES (AES-NI).** Software AES first; if
  benchmarks justify it later, a runtime-probed AES-NI path lives
  behind a feature flag. The plan deliberately keeps the production
  binary single-implementation.

## Verifying primitives

Every primitive in [`src/crypto/`](../src/crypto.rs) ships with a
differential test suite that runs ≥ 1000 random inputs through both
peel's implementation and the upstream reference crate, asserting
byte-identical output. The corpus also includes the known-answer
vectors from the format specs themselves (FIPS 197 for AES, RFC 3174
for SHA-1, NIST SP 800-132 for PBKDF2). To reproduce:

```sh
cargo test --tests test_crypto_diff
```

The reference crates this test pins against are listed in
[`Cargo.toml`](../Cargo.toml) under `[dev-dependencies]`: `sha1`,
`hmac`, `pbkdf2`, `aes`, `ctr`, `cbc`. The runtime binary links **none**
of these.

## See also

- [`PLAN_archive_encryption.md`](PLAN_archive_encryption.md) — the
  implementation plan this feature traces against.
- [`ENGINEERING_STANDARDS.md`](ENGINEERING_STANDARDS.md) §Dependency
  Policy — the "std-first, vetted-crates second, novel-deps almost
  never" rule that motivates the hand-rolled crypto module.
- [`src/encryption.rs`](../src/encryption.rs) — the shared
  `EncryptionError` enum.
- [`src/secret.rs`](../src/secret.rs) — the `Password` zeroising
  wrapper and `--password-from` parser.
