inro

Doc 5 of 5

Encryption

Written from the engine design specification. The engine is being implemented; this describes the design, not a shipped API.

Scope

Optional, behind a compile-time feature, and fixed at file creation: turning it on later would mean rewriting every page.

Algorithm

ChaCha20-Poly1305 by default, AES-256-GCM selectable. The default follows the split across target environments: with hardware acceleration, AES-GCM reaches 1-2 GB/s and encrypting a 4 KB page costs 2-4 μs; without it — IoT processors with no crypto extensions — software AES falls to 50-100 MB/s, and that same page costs 40-80 μs. ChaCha20 without acceleration holds 200-400 MB/s, i.e. 10-20 μs.

Both use RustCrypto's implementations — pure Rust, no_std-compatible — adding 60-100 KB to the binary, compiled in only when the feature is active. inro does not implement its own cryptographic primitives: the dependency saved doesn't offset the risk.

This is the one exception to the project's zero-dependency default, and it carries a caveat worth stating plainly: forbid(unsafe_code) does not reach dependencies, and these crates use unsafe in CPU feature detection and in their SIMD backends. Phase 1 has to evaluate whether ChaCha20's portable backend can drop that unsafe, and at what performance cost — measured, not assumed.

Encrypted page layout

0   8   txn_id                (plaintext: needed to derive the nonce)
8   16  authentication tag
24  ..  ciphertext of the full logical page

The nonce is derived, not stored: nonce = page_number (u32) || txn_id (u64), exactly 12 bytes. The tag replaces the CRC32, which becomes redundant — a corrupted or tampered page fails authentication instead.

Net overhead: 20 bytes per page — 0.5% with 4 KB pages, 3.9% with 512 B pages. A 31 MB node with 4 KB pages pays 155 KB.

The nonce invariant

page || txn_id is unique as long as a page is not written twice inside the same transaction. The copy-on-write design satisfies this: a dirty page is modified in RAM and flushed exactly once, on commit.

If an intermediate flush inside a transaction were ever introduced, the nonce would repeat, and with either ChaCha20-Poly1305 or AES-GCM that breaks the confidentiality of both pages and can allow the tag to be forged. It stays a documented invariant, with an automated test that fails if it's ever violated.

Key management

The engine receives 32 bytes, already derived. Deriving from a password is out of scope: it would mean pulling in Argon2 — heavy on RAM, unsuitable for IoT — or PBKDF2, and those are decisions that don't belong to a storage engine. On the edge the key arrives from the control plane at provisioning; on IoT, from secure boot or configuration.

Rotation works by wrapping: the key the engine receives is the master key, and it wraps a per-file data key, stored encrypted in both meta pages. Rotating the master rewrites those bytes in the two metas, not the whole file. Rotating the data key does mean rewriting the file, and is only needed if it's suspected to have leaked.

What it does not protect

The 64-byte header stays in plaintext, because the page size has to be known before anything can be decrypted. What leaks: that the file is an inro database, its format version, its geometry, its total size, the number of transactions performed, and the access pattern. Keys and values inside the pages are encrypted, so neither their lengths nor their content leak.

Encryption protects against disk theft, copying a backup, and access by the hosting provider. It does not protect against a compromised process, because the key lives in its memory.