Skip to content

Protocol

Wire formats and key derivation

Naming X3DH and the Double Ratchet proves nothing on its own - a protocol is the composition, not the ingredient list. These are the actual formats and labels from the shipping source, including the two places where the composition is weaker than the primitives.

Everything below is read off the code in the public repository, client and relay alike. Where something is unproven or missing, it is marked rather than omitted.

Wire formats

Session-init envelope

[0x01] [IK_A: 32] [EK_A: 32] [EncryptedMessage]

First message to a contact. Carries the sender identity and ephemeral public keys so a stranger can be decrypted with no prior contact record. Base64-encoded into the Solana memo.

Regular envelope

[0x00] [EncryptedMessage]

Every subsequent message. The session is already established, so no key material rides along.

Prekey bundle

[flags: 1] [IK: 32] [SPK: 32] [SPK signature: 64] (+[OPK: 32] if bit0)

Published to the on-chain registry. Bit 0 signals a one-time prekey. The shipping client always publishes it unset - see the gap below.

Padding buckets

32 · 64 · 128 · 256 · 512 bytes

Plaintext is padded to the next bucket before encryption. The 512 ceiling exists because a larger memo exceeds Solana’s 1232-byte transaction limit.

Key derivation

PrivaMesh key derivation functions and labels
Identity derivationHKDF-SHA256(IKM = phrase, salt = "PrivaMesh-msg-identity-v1", info = label) for labels dhIdentityKey, signingKey, signedPrekey and pqPrekey.
Root key ratchetKDF_RK = HKDF-SHA256(salt = root key, IKM = DH output, info = "PrivaMesh-DR-RK").
Stealth addressaddress(root, label, index) = HKDF-SHA256(IKM = root, salt = label, info = index) → Ed25519 public key. label fixes the direction, index is a per-direction counter.
Payload encryptionAES-256-GCM under the per-message key produced by the symmetric ratchet.

Relay endpoints

Fee worker endpoints and payloads
EndpointBodyWhat it does
GET /pubkeyThe blind-signature issuer public key (N, E). The private exponent stays a Worker secret.
POST /issue{ jws, blinded }Verifies an Apple receipt and returns blind signatures. This is where payment is proven - once.
POST /send{ tx, token }Anonymous path. Verifies the token is valid and unspent, co-signs the fee-payer slot, submits. No account is sent.
POST /send{ tx, jws, account }Legacy path, and the only one accepted for publishing a public discovery nickname.
POST /credit{ jws, account }Adds a consumable message pack to an account balance.

Abuse controls sit in the same Worker: a per-caller sliding-window rate limit defaulting to 20 sends a minute, a global daily cap on sponsored transactions, and atomic single-spend tracking so one token cannot fund two sends. A treasury guard refuses to co-sign any transaction that would debit the treasury rather than only pay its fee.

Where the composition is weaker than the primitives

One-time prekeys are never published. The bundle format reserves a flag and a slot for them, and the client always sends the slot empty. Combined with identity, signed prekey and PQ prekey all being HKDF-derived from the recovery phrase, this means a phrase holder can recompute the X3DH root for the session-opening envelope of every conversation - and those envelopes are on the chain permanently. This is documented as a limitation rather than argued away. Fixing it needs consumed one-time prekeys or a rotating non-deterministic signed prekey.

There is no user-facing key verification. Prekey bundles are signed, so the registry cannot hand out a key that does not belong to the publishing account. But nothing lets you confirm that the account belongs to the person you meant to add. There is no safety number, no fingerprint comparison and no out-of-band check in the shipping app. Against an attacker who can get you to add the wrong account, the signature does not help.

What would actually constitute proof

None of the above is proof, and it is not offered as such. What would be: published test vectors another implementation can verify itself against; a formal or symbolic analysis of this composition rather than of its parts; and an independent audit of the implementation. None of the three exists yet. Until they do, this page is a description you can check against the source, which is a weaker claim than a proof and an honest one.

Frequently asked questions

Are there test vectors?

Not published yet. Wire formats and KDF labels are documented here and readable in the source, but there is no vector file another implementation could check itself against. That is the next thing this page needs.

Has the protocol been formally analysed?

No. The primitives - X3DH, the Double Ratchet, AES-256-GCM, ML-KEM-768 - carry their own analysis. This particular composition of them has had no formal treatment and no independent audit.

Why is naming X3DH and the Double Ratchet not enough?

Because a protocol is the composition, not the ingredient list. Where key material comes from, whether one-time prekeys are consumed, how the transport interacts with retries - those decide whether the properties hold. Two of ours are documented below as open.

Where is the fee worker source?

In the same public repository as the client, under relay/. It is a Cloudflare Worker: blind-token issuance and verification, single-spend tracking, quota and rate limits, and transaction co-signing.

Primary sources

Keep reading