The crypto boundary
View SourceAmarula's cryptography is pure and self-contained: it implements the Noise_XX handshake and the Signal Protocol (1:1 Double Ratchet + group sender keys) with no dependency on WhatsApp, transport, or persistence. Everything WhatsApp-specific lives above it and reaches the crypto only through a thin, explicit seam.
This document draws that line. It is a real architectural contract — the core must never depend on the app — and it is what would make the crypto extractable as a standalone library (a pure-Elixir Signal/Noise implementation) without untangling.
The three layers
┌──────────────────────────────────────────────────────────────┐
│ App (Amarula.Connection, Messages, Storage, …) │ WhatsApp client
├──────────────────────────────────────────────────────────────┤
│ Glue — the ONLY bridge: persistence + WhatsApp multi-device │ 5 + 1 modules
├──────────────────────────────────────────────────────────────┤
│ Core — pure Signal / Noise crypto. No Amarula.* app deps. │ extraction-ready
└──────────────────────────────────────────────────────────────┘The dependency rule: arrows point down only. App → Glue → Core. The Core never
references the Glue or the App; the Glue never reaches back into the App's
transport. A Amarula.Conn / Amarula.Storage reference inside a Core module is a
boundary violation.
Core — pure crypto (no app coupling)
Depends only on Erlang :crypto, Bitwise, and other Core modules. Given keys and
bytes in, it returns keys and bytes out — it neither loads nor saves anything, and
knows nothing about JIDs, LIDs, sockets, or WhatsApp.
lib/amarula/protocol/crypto/
crypto.ex— primitives (X25519, AES-GCM/CBC/CTR, HKDF, HMAC, random).noise_handler.ex— the Noise_XX handshake state machine.xeddsa.ex— XEdDSA sign/verify over X25519 keys.constants.ex— protocol constants.
lib/amarula/protocol/signal/ (1:1 Double Ratchet)
session_cipher.ex— encrypt/decrypt + ratchet advance (pure: record in, record out).session_builder.ex— X3DH session establishment.session_record.ex— the session/ratchet data structure.whisper_protocol.ex— WhisperMessage wire encode/decode.crypto_helpers.ex— Signal key-derivation helpers.pre_keys.ex— prekey generation.lid_mapping.ex— the pure LID/PN encode/decode (not the store).repository.ex,types.ex— shared structs/contracts.key_store_behaviour.ex,sender_key_store_behaviour.ex— the behaviours the Glue implements (the seam is defined here, in Core, and satisfied above).
lib/amarula/protocol/signal/group/ (group sender-key cipher)
- everything except
sender_key_store.ex:group_cipher,group_session_builder,key_helper,sender_chain_key,sender_key_distribution_message,sender_key_message,sender_key_name,sender_key_record,sender_key_state,sender_message_key.
Glue — the only bridge (persistence + WhatsApp multi-device)
These are the only modules allowed to touch both Core crypto and
Amarula.Conn / Amarula.Storage. They are adapters: they persist Core's records,
and they encode WhatsApp's multi-device concepts (LID ↔ PN, device lists) that are
not part of Signal. They belong to Amarula, not to a standalone crypto library.
signal/session_store.ex— persistsSessionRecords viaAmarula.Storage(implements the Core session-store seam).signal/group/sender_key_store.ex— persists sender-key records (the group seam).signal/session_injector.ex— installs fetched prekey bundles into sessions.signal/device_list_cache.ex— caches a peer's device list (a WhatsApp concept).signal/lid_mapping_store.ex/lid_mapping_file_store.ex— persist LID ↔ PN mappings (WhatsApp multi-device addressing).
Why the seam is a behaviour
Core defines KeyStoreBehaviour / SenderKeyStoreBehaviour (and the session store
shape) and depends on those contracts, not on a concrete store. The Glue provides
the concrete, Amarula.Storage-backed implementations. So Core is parameterised
over persistence: hand it any conforming store and it runs — which is exactly what
a standalone library needs.
Status & extraction
The boundary is already honored in the code (verified: no Core module
references Amarula.Conn/Amarula.Storage). This doc makes it an explicit,
enforced-by-review contract rather than an accident.
The Core is therefore extraction-ready in principle — it could become a
standalone pure-Elixir Signal/Noise library, with the cross-language tests
(session_cipher_crosslang_test, session_builder_crosslang_test) as its proof of
byte-compatibility with libsignal. Extraction is not planned for now: it carries
a real maintenance and security-expectation cost (see the discussion in the project
notes), and the Glue would stay here regardless. This document is the prerequisite —
draw and hold the line first; decide on extraction later.
Keeping the line
When adding or changing a crypto module, ask: does it need Amarula.Conn or
Amarula.Storage?
- No → it's Core. Keep it pure; depend only on
:cryptoand other Core modules. - Yes → it's Glue. It implements a Core behaviour and lives at the seam; it must not leak app/transport concerns down into Core.
If a Core module starts needing storage, that's a signal to split it: the pure algorithm stays in Core, the persistence moves to a Glue adapter behind a behaviour.