Skip to content

Latest commit

 

History

History
141 lines (117 loc) · 7.14 KB

File metadata and controls

141 lines (117 loc) · 7.14 KB

Encryption Overview

Cryptspeak, csCardputer's at-rest encryption layer, adds mandatory at-rest encryption for everything the firmware writes to flash, NVS, and SD card. It does not change anything about the Reticulum/LXMF wire protocol — packets on LoRa, WiFi, or TCP keep using their own protocol-level crypto, unmodified. This is purely about making a raw dump of the device's storage (flash chip desoldered, SD card pulled, NVS partition extracted) useless to an attacker who doesn't know the password.

The overall approach (wrap the identity with a password-derived key, derive everything else from the identity) builds on the idea and proof-of-concept in konsumer's arduino-rns-encrypted-store and arduino-rns-password.

What's protected

Data category Storage Encrypted? Module
Identity private key Flash, NVS, SD Yes IdentityCrypto
LXMF message bodies Flash, SD Yes MessageEncryption
Saved contacts + name cache Flash, SD Yes ContactsEncryption
Device settings (WiFi passwords, TCP hubs, radio config, display name) Flash, NVS, SD Yes SettingsEncryption
microReticulum's known_destinations table (announced peer names, cached independently of the app's own contacts/name-cache files) Flash Yes KnownDestEncryption
Announces / destination hashes on the air Not applicable — public by Reticulum's own design
Reticulum/LXMF packets in flight Unchanged — protocol's own crypto

Every category above went through the same envelope; there is no unencrypted category left except the color theme (see theme-config.md for why that one is deliberately excluded). See threat-model.md for the full adversarial reasoning.

One engine, five domains

All five protected categories — identity, messages, contacts, settings, known-destinations — use the same authenticated-encryption envelope (AES-256-CTR + HMAC-SHA256, encrypt-then-MAC). Identity has its own implementation (IdentityCrypto) because it alone derives its key from the user's password via PBKDF2 (it has to — the identity key is the thing that makes every other domain's key possible). The other four domains derive their keys from the identity's private key via HKDF, so they share one generic implementation:

src/storage/AtRestCrypto.{h,cpp}       ← shared envelope engine
src/storage/MessageEncryption.{h,cpp}  ← domain: messages           (magic "RMS1")
src/storage/ContactsEncryption.{h,cpp} ← domain: contacts           (magic "RCN1")
src/storage/SettingsEncryption.{h,cpp} ← domain: settings           (magic "RUC1")
src/storage/KnownDestEncryption.{h,cpp}← domain: known-destinations (magic "RKD1")

AtRestCrypto is the shared envelope engine: the same encrypt/decrypt/MAC logic used by every domain, parameterized by a 4-byte magic and an HKDF "info" string per domain rather than duplicating the ~150 lines of crypto per file.

Why separate domains instead of one shared key?

Each domain's encryption key is derived as:

keys = HKDF-SHA256(64 bytes,
                    ikm  = identity.encryptionPrivateKey(),
                    salt = identity.hash(),
                    info = "<domain-specific string>")
keys[0:32]  → AES-256-CTR key
keys[32:64] → HMAC-SHA256 key

The info string namespaces the derivation per domain:

Domain Magic HKDF info
Messages RMS1 rscardputer.msg.v1
Contacts RCN1 rscardputer.contacts.v1
Settings RUC1 rscardputer.settings.v1
Known-destinations RKD1 rscardputer.knowndest.v1

A key derived for one domain cannot decrypt another, even though all four ultimately come from the same identity private key. This means a bug or future attack that recovers the message key, say, doesn't also hand over your WiFi password.

Envelope format (shared by messages, contacts, settings)

┌─────────┬──────┬──────┬──────────────┬─────────────┐
│ magic   │ ver  │ IV   │ ciphertext   │ HMAC-SHA256 │
│ 4 bytes │ 1 B  │ 16 B │ N bytes      │ 32 bytes    │
└─────────┴──────┴──────┴──────────────┴─────────────┘
  • Encrypt-then-MAC: the HMAC covers magic | version | IV | ciphertext. On decrypt, the MAC is checked with a constant-time comparison before any decryption happens. Wrong identity, bit-rot, or deliberate tampering all fail the same way — no plaintext is ever produced from data that doesn't authenticate.
  • Fresh random 16-byte IV per write. AES-256-CTR reuses a key safely as long as the IV never repeats; with a 128-bit IV space the birthday bound is astronomically larger than anything this device could ever write.
  • Legacy passthrough. A buffer that doesn't start with the domain's magic (or is shorter than the 53-byte minimum envelope size) is treated as a pre-encryption plaintext file and returned unchanged. This is what makes the rollout non-breaking — see boot-sequence.md for how existing plaintext data gets upgraded.

Crypto primitives in use

All from the vendored rweather Crypto library (no external dependency added):

Purpose Primitive
Bulk cipher AES-256, CTR mode
Authentication HMAC-SHA256
Key derivation (identity → domain keys) HKDF-SHA256
Key derivation (password → KEK) PBKDF2-HMAC-SHA256 (hand-rolled in IdentityCrypto, see encryption-identity.md)
Randomness (IVs, salts) microReticulum's RNS::Cryptography::random()

The library also vendors ChaCha20/Poly1305, GCM, EAX, SHA-3/Keccak, BLAKE2, and Ed25519/Curve25519 (used by Reticulum's own identity/transport crypto, not by this at-rest layer). They're available if a future domain needs them, but AES-256-CTR+HMAC was kept for the new domains specifically to match the existing, already-reviewed identity/message design rather than introduce a second authenticated-encryption construction.

Read next