Latch is an Erlang library for secure messages between two peers.
Each message uses a fresh key from a ratchet. The ratchet runs on the
standard crypto module. Latch has no external dependencies.
Latch sets up a shared secret between two peers. The setup uses X25519 key agreement. The shared secret seeds a double ratchet session.
The double ratchet advances a fresh key for every message. Messages use AES-256-GCM authenticated encryption. The header of each message authenticates the ciphertext.
This release adds fingerprint confirmation. Each peer can read a grouped hex fingerprint of the remote signing key. Peers confirm the fingerprint out of band before they trust the session. The release keeps three earlier capabilities:
- A signed handshake that authenticates identities.
- Out-of-order message handling with skipped keys.
- Group messages with a shared sender key.
The handshake authenticates both peers. Each peer signs its ephemeral key with an Ed25519 identity key. The other peer verifies the signature and rejects a bad key.
Forward secrecy holds. After a DH ratchet, old message keys no longer exist. A captured old ciphertext fails authentication.
Authenticated encryption detects tampering. A changed ciphertext or header fails decryption.
Authentication is self-managed. There is no external trust root. Compare fingerprints out of band to prevent impersonation.
Each peer holds an identity key and an ephemeral key. Peers exchange the public halves over the transport. Both peers derive the same shared secret.
The handshake hello carries the identity key, the signing key, the ephemeral key, and a signature over those three keys. A peer verifies the signature before it derives a session.
The double ratchet has two parts:
- The symmetric ratchet derives one message key per message.
- The DH ratchet derives new root and chain keys from a fresh X25519 output.
The initiator performs the first DH ratchet at session start. The responder runs its first DH ratchet on the first received message. The ratchet then alternates with each reply.
The receiving side stores skipped message keys. It decrypts out-of-order
messages and gaps up to max_skip. It rejects older messages. Messages from
a superseded chain that arrive after a DH ratchet are dropped.
Group messaging uses a sender key. Each member owns one sending chain. The other members hold receiver sessions for that chain. Every message carries an Ed25519 signature.
After the handshake, call latch_peer:fingerprint/1 to read the remote peer's
signing-key fingerprint. The value is a SHA-256 digest shown as 16 groups of
four hex digits.
Compare the fingerprint out of band. Call latch_peer:confirm_fingerprint/2
with the expected value. The function accepts any case and spacing. It returns
ok on a match or {error, mismatch} when the value differs.
You can also format a raw signing key with latch_agreement:fingerprint/1.
Use latch_agreement:fingerprints_match/2 to compare two strings.
src/ library modules
latch.erl top-level API and demo entry point
latch_kdf.erl HKDF and ratchet key derivation
latch_crypto.erl AES-256-GCM wrapper
latch_agreement.erl key agreement, identity signing, session setup
latch_session.erl the double ratchet session
latch_sender_key.erl group messaging with a sender key
latch_wire.erl socket framing
latch_peer.erl a peer that talks over a TCP socket
latch_demo.erl the local demos
test/ EUnit tests
include/latch.hrl shared record definitions
.github/workflows continuous integration
- Erlang/OTP 26 or newer.
- rebar3 3.22 or newer.
Run this command from the repository root:
rebar3 compile
Run this command from the repository root:
rebar3 eunit
The suite runs 82 tests. It covers key derivation, authenticated encryption, the signed handshake, fingerprint confirmation, the ratchet, out-of-order delivery, forward secrecy, group messaging, and the socket transport.
Run the static analysis:
rebar3 dialyzer
Dialyzer reports no warnings.
The demos run on one machine over loopback TCP. No secrets are needed.
Build the demo script once:
rebar3 escriptize
Two peers exchange messages over a direct socket. The ratchet advances with every reply.
./_build/default/bin/latch pairwise
Sample output:
Latch pairwise demo
-------------------
Two peers over a loopback TCP socket
bob listens on 127.0.0.1:60004
handshake complete. alice=initiator bob=responder
identities verified with ed25519 signatures
alice sees peer as 7DBBE137
fingerprint A1B2 C3D4 E5F6 7081 92A3 B4C5 D6E7 F809 ...
bob sees peer as 0DC10A4E
fingerprint 1F2E 3D4C 5B6A 7988 8776 6554 4332 2110 ...
[0] fingerprint confirmation
alice confirms bob: matched
bob confirms alice: matched
both peers matched the fingerprints they received
[1] conversation with 8 messages each way
alice -> : alice message 01 [n=0]
alice -> : alice message 02 [n=1]
bob <- : alice message 01 [dh=2A94FCD9 n=0 pn=0]
bob <- : alice message 02 [dh=2A94FCD9 n=1 pn=0]
bob -> : bob message 01 [n=0]
bob -> : bob message 02 [n=1]
alice <- : bob message 01 [dh=93C22FA0 n=0 pn=0]
alice <- : bob message 02 [dh=93C22FA0 n=1 pn=0]
alice -> : alice message 03 [n=0]
bob <- : alice message 03 [dh=D9BB5FA1 n=0 pn=2] [DH ratchet]
...
[2] session summary
alice root=9D864614 dh_send=4A7CCC4B dh_recv=CE203122 send=0 recv=2
bob root=F734C290 dh_send=CE203122 dh_recv=51A0ABB6 send=2 recv=2
[3] forward secrecy check
bob root ratchet: A2794B19... -> F734C290...
bob tries to decrypt message 1 again with the current session:
failed. the old message key is gone.
The dh value changes with each ratchet. The pn value reports the previous
chain length. The fingerprint is the remote peer's signing key. Read it aloud
to confirm the peer out of band. The forward secrecy check shows that the
first message key no longer exists.
A relay delivers frames out of order. Bob decrypts them with skipped keys.
./_build/default/bin/latch shuffled
Sample output:
Latch out-of-order demo
-----------------------
A relay delivers frames out of order
[1] alice sends 9 messages in one sending chain
[2] bob receives them in shuffled order
bob <- : alice message 03 [n=2] (skipped store: 0)
bob <- : alice message 02 [n=1] (skipped store: 2)
bob <- : alice message 01 [n=0] (skipped store: 1)
bob <- : alice message 06 [n=5] (skipped store: 0)
...
[3] result
all 9 messages decrypted. max skipped keys held: 2
bob session: 0 skipped keys remain
The skipped store grows when Bob jumps ahead. It shrinks when Bob consumes the stored keys.
Three members chat over a TCP hub. Each member uses its own sender key.
./_build/default/bin/latch group
Sample output:
Latch group demo
----------------
Three members, one sender key per member
a short chat over the hub
alice -> group: hello everyone, this is alice
bob <- group (alice): hello everyone, this is alice
carol <- group (alice): hello everyone, this is alice
bob -> group: hi alice, bob here
...
Print the runtime environment:
./_build/default/bin/latch info
- EUnit: 82 tests, 0 failures.
- Dialyzer: no warnings.
- Demos: all three run locally on loopback TCP.
- Authentication is self-managed. There is no external trust root.
- Fingerprint confirmation needs a separate channel. The TCP socket does not prove the peer alone.
- The skip window covers one sending chain. Late messages from a superseded chain are dropped.
- Sessions live in memory. There is no persistence.
- The demos use loopback TCP. They do not use TLS.
Complete:
- Sign the handshake and authenticate identities.
- Add a fingerprint confirmation flow for peer keys.
Remaining:
- Add session persistence and resumption.
- Add test vectors from the Double Ratchet reference.
- Add a supervised peer with auto-reconnect.
MIT. See LICENSE.