inro

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   nonce counter         (plaintext: needed to re-derive the nonce)
8   16  authentication tag
24  ..  ciphertext of the full logical page

The nonce is derived, not stored: nonce = page_number (u32) || counter (u64), exactly 12 bytes. The counter is a monotonic, persisted write counter — not the transaction's txn_id — reserved ahead of time for each page write and tracked in both meta pages. 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 || counter is unique as long as the counter is never reused for the same page — a guarantee enforced by whoever hands out counters, not something the copy-on-write design provides by itself.

An earlier version of this design derived the nonce from page || txn_id, on the reasoning that copy-on-write already guarantees uniqueness: a dirty page is modified in RAM and flushed exactly once, on commit. That holds for a transaction that writes each page at most once — but txn_id is the same for every page in a transaction, and a page can be written more than once inside a single transaction (freeing a page and reallocating it before commit reuses its page number), which would reuse that txn_id and therefore the nonce. inro reserves a dedicated write counter per page write instead, so that case can't happen: the pair (page, counter) never repeats, no matter how many times the same page is rewritten inside one transaction.

If the counter were ever reused — a bug in how it's reserved, or an intermediate flush that doesn't advance it — that breaks the confidentiality of both pages under either ChaCha20-Poly1305 or AES-GCM, 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.