Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ ignition/deployments/chain-84532
.vercel/
/tests/scan-engine.test.ts
/tests/scan-test-report.md


#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
4 changes: 2 additions & 2 deletions apps/docs/docs/concepts/handshake.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ For a deeper analysis of forward secrecy, post-compromise security, and post-qua

<sup>1</sup> A planned **Contact KEM** integration will allow recipients to publish ML-KEM keys on-chain, enabling hybrid-encrypted first-message payloads. Under the hood the 2-step handshake flow remains unchanged, but first contact will be protected, hence simulating an offline initiation.

<sup>2</sup> Only ephemeral keys participate in key derivation: `SK = KDF(DH(EKa, EKb) || KEM_SS)`. Even if all long-term keys are later compromised, past sessions are unrecoverable because both ephemeral secrets were deleted after use. (Differently, in Signal, if both Bob's identity key and signed prekey are compromised, all sessions established under that prekey are recoverable, and althogh this is mitigated via prekey rotation, there is always an active window.)
<sup>2</sup> Only ephemeral keys participate in key derivation: `SK = KDF(DH(EKa, EKb) || KEM_SS)`. Even if all long-term keys are later compromised, past sessions are unrecoverable because both ephemeral secrets were deleted after use. (Differently, in Signal, if both Bob's identity key and signed prekey are compromised, all sessions established under that prekey are recoverable, and although this is mitigated via prekey rotation, there is always an active window.)



## Next Steps

- [Protocol Flow](../how-it-works/protocol-flow.md) — the full step-by-step exchange, on-chain events, and code
- [Double Ratchet](./ratchet/double-ratchet.md) — what happens after the handshake
- [Wire Format](../how-it-works/wire-format.md) — how messages are encoded on-chain
- [Wire Formats](../how-it-works/wire-formats.md) — how messages are encoded on-chain
11 changes: 6 additions & 5 deletions apps/docs/docs/concepts/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Identity

# Identity

In decentralized systems, identity is not as straightforward as it may seem. On blockchains, users are represented by addresses, but messaging protocols rely on encryption and signing keys that may exist indipendently.
In decentralized systems, identity is not as straightforward as it may seem. On blockchains, users are represented by addresses, but messaging protocols rely on encryption and signing keys that may exist independently.

Verbeth binds cryptographic messaging keys to Ethereum addresses. A single wallet signature produces all keys, and a signed proof ties them to your address.

Expand Down Expand Up @@ -57,10 +57,11 @@ A single wallet signature seeds the entire key hierarchy:
* The X25519 identity key is a stable, long-term public key bound to the EOA.
The double ratchet uses ephemeral keys for forward secrecy,
but this key serves as part of the published identity (like a prekey)
for initial key agreement, session reset re-encryption, and any protocol
extension needing an address-bound X25519 key.
for future contact key discovery, session reset re-encryption, and any
protocol extension needing an address-bound X25519 key.
** The session key is always derived but only used by apps that delegate
transactions to a Safe module (e.g. gasless messaging).
transactions to a Safe module. The session signer submits a normal
transaction and pays gas, or a relay service can sponsor it.
```

Properties:
Expand Down Expand Up @@ -162,4 +163,4 @@ signature = Ed25519.sign_detached(header ‖ ciphertext, ed25519_secret_key)

The recipient independently reconstructs the signed blob from the header and ciphertext, then verifies.

See [Wire Format](../how-it-works/wire-format.md) for the full binary layout and verification order.
See [Wire Formats](../how-it-works/wire-formats.md) for the full binary layout and verification order.
12 changes: 6 additions & 6 deletions apps/docs/docs/concepts/ratchet/double-ratchet.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ The algorithm combines two ratcheting mechanisms:
v v v
┌────────┐ ┌────────┐ ┌────────┐
| CK 0 | | CK 0 | | CK 0 |
| → MK₀ | | → MK₀ | | → MK₀ | <-- Symmetric chain ratchet
| → MK₁ | | → MK₁ | | → MK₁ | (per message)
| → MK₂ | | → ... | | → ... |
| → MK_0| | → MK_0| | → MK_0| <-- Symmetric chain ratchet
| → MK_1| | → MK_1| | → MK_1| (per message)
| → MK_2| | → ... | | → ... |
└────────┘ └────────┘ └────────┘
```

Expand Down Expand Up @@ -55,7 +55,7 @@ Each ratchet session tracks:
| `skippedKeys` | Stored keys for out-of-order messages |
| Topic fields | Current, next, and previous topics per direction |

**Session state must be persisted after every encrypt/decrypt operation.** Rolling back to stale state creates duplicate message keys and breaks security guarantees. See [Ratchet Internals](../../how-it-works/ratchet-internals.md) for the full TypeScript interface.
**Session state must be persisted after every encrypt/decrypt operation.** Rolling back to stale state creates duplicate message keys and breaks security guarantees. See [Ratcheting](../../how-it-works/ratcheting.md) for the full TypeScript interface.

### Duplicate message replay

Expand All @@ -76,5 +76,5 @@ So, these bounds prevent DoS via malicious skip counts while tolerating real-wor
## Next steps

- [Topic Ratcheting](./topic-ratcheting.md) -- how conversation topics evolve for metadata privacy
- [Ratchet Internals](../../how-it-works/ratchet-internals.md) -- KDFs, code, full session state
- [Wire Format](../../how-it-works/wire-format.md) -- binary layout of ratchet messages
- [Ratcheting](../../how-it-works/ratcheting.md) -- KDFs, code, full session state
- [Wire Formats](../../how-it-works/wire-formats.md) -- binary layout of ratchet messages
20 changes: 10 additions & 10 deletions apps/docs/docs/concepts/ratchet/topic-ratcheting.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ title: Topic Ratcheting

# Topic Ratcheting

Topics are like the the on-chain addresses of a given conversation. Every `MessageSent` event is indexed by a `topic` field, and this is how clients find messages meant for them. Topics are derived from shared secrets, so only participants know which topics belong to their conversation.
Topics are like the on-chain addresses of a given conversation. Every `MessageSent` event is indexed by a `topic` field, and this is how clients find messages meant for them. Topics are derived from shared secrets, so only participants know which topics belong to their conversation.

The reason for this is that verbeth cares about forward secrecy of metadata, not just content. Each DH ratchet step produces new topics, breaking the link between conversation epochs:

```
Epoch 0 (Handshake) Epoch 1 (Alice ratchets) Epoch 2 (Bob ratchets)
─────────────────── ────────────────────── ─────────────────────
topicOut_A = H(rk₀, dh₀) topicOut_A = H(rk₁, dh₁) topicOut_A = H(rk₂, dh₂)
topicIn_A = H(rk₀, dh₀) topicIn_A = H(rk₁, dh₁) topicIn_A = H(rk₂, dh₂)
unlinkable unlinkable unlinkable
Epoch 0 (Handshake) Epoch 1 (Alice ratchets) Epoch 2 (Bob ratchets)
──────────────────────────────────────── ──────────────────────────────────────── ─────────────────────
topicOut_A = H(rk_0, dh_0, "outbound") topicOut_A = H(rk_1, dh_1, "outbound") topicOut_A = H(rk_2, dh_2, "outbound")
topicIn_A = H(rk_0, dh_0, "inbound") topicIn_A = H(rk_1, dh_1, "inbound") topicIn_A = H(rk_2, dh_2, "inbound")
unlinkable unlinkable unlinkable
```

>An observer watching the blockchain sees messages appear on different topics over time with no way to connect them across epochs. Within a single epoch, consecutive messages from the same party share the same topic, but this reveals nothing beyond the fact that the same party sent multiple messages in the same conversation, which is already implied by the topic existing at all.
Expand All @@ -28,15 +28,15 @@ Each DH ratchet step feeds the new DH output and current root key into a topic d
rootKey (PQ-secure salt) + dhOutput
|
v
HKDF(dhOutput, rootKey, "verbeth:topic-{direction}:v3", 32)
HKDF(dhOutput, rootKey, "verbeth:topic-{direction}", 32)
|
v
keccak256 → topic (bytes32)
```

The root key acts as HKDF salt. Because the root key descends from the [Handshake](../handshake.md)'s hybrid secret (X25519 + ML-KEM), topic derivation is quantum-resistant, meaning that even if X25519 is broken, topics from different epochs cannot be linked without the root key.

Direction is either `outbound` or `inbound`, so each party sends and listens on different topics. See [Ratchet Internals](../../how-it-works/ratchet-internals.md#topic-derivation) for the exact KDF code.
Direction is either `outbound` or `inbound`, so each party sends and listens on different topics. See [Ratcheting](../../how-it-works/ratcheting.md) for the exact KDF code.

## Grace period

Expand All @@ -48,5 +48,5 @@ To handle this, the SDK retains the previous inbound topic after a topic transit
## Next steps

- [Double Ratchet](./double-ratchet.md) -- the algorithm that drives topic evolution
- [Ratchet Internals](../../how-it-works/ratchet-internals.md) -- KDF code and full session state
- [Ratcheting](../../how-it-works/ratcheting.md) -- KDF code and full session state
- [Metadata Privacy](../security/metadata-privacy.md) -- threat analysis including metadata privacy
8 changes: 8 additions & 0 deletions apps/docs/docs/concepts/security/cryptographic-guarantees.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ This is an honest limitation shared by all major double-ratchet protocols today.
For how PQ security compares across protocols, see [here](../handshake.md#other-pq-secure-handshake-protocols).


### Symmetric Encryption: XSalsa20-Poly1305

After each ratchet step, the derived message key is used to encrypt the payload with XSalsa20-Poly1305 (`nacl.secretbox`). The message key is 32 bytes (256-bit), produced by HMAC-SHA256 over the current chain key.

XSalsa20-Poly1305 is post-quantum safe for symmetric encryption. Grover's algorithm, the most relevant quantum attack against symmetric ciphers, provides at most a quadratic speedup, reducing the effective key strength from 256 to 128 bits. 128-bit post-quantum security is above the accepted security threshold.

This means that even if a future quantum adversary records ciphertext today, they cannot brute-force message keys derived from the ratchet chain, the symmetric layer remains secure regardless of quantum advances.

### Limitations recap

- After the hybrid handshake, ongoing ratchet re-keying uses X25519 only. So Verbeth stays HNDL-resistant against passive recording, because later keys still descend from the original PQ-secure root key. But it does not provide full post-quantum PCS after a live state compromise, since recovery would rely on new X25519 ratchet steps rather than a fresh PQ exchange.
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/docs/concepts/security/metadata-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Let's say 0x123 emits a `Handshake` whose `recipientHash` is `hash(0x789)`, and

**Against a quantum adversary**: A quantum computer could solve the X25519 DH from the public keys. But the tag derivation *also* requires the ML-KEM shared secret. The ML-KEM ciphertext is inside the encrypted response payload which cannot be decrypted without 0x123's ephemeral secret. So even a quantum adversary cannot link the handshake to its response.

For the exact tag computation, see [Protocol Flow](../../how-it-works/protocol-flow.md#hybrid-tag-computation).
For the exact tag computation, see [Protocol Flow](../../how-it-works/protocol-flow.md#two-keypairs-in-the-response).

## Handshake-to-Message Unlinkability

Expand Down Expand Up @@ -56,4 +56,8 @@ When you query for messages, the RPC sees which topics you subscribe to, when yo
- **Decoy queries**: noise injection to obscure real interest patterns
- **Query aggregation**: batching queries across multiple topics to reduce per-topic signal

A future integration of [Labeled Private Set Intersection](../../roadmap/metadata-privacy-psi.md) will allow clients to query an untrusted indexer without revealing which topics or recipients they are interested in. The indexer sees only opaque, homomorphically encrypted queries, making topic-level correlation infeasible even for a fully malicious provider.
A future integration of will allow clients to query an untrusted indexer without revealing which topics or recipients they are interested in. The indexer sees only opaque, homomorphically encrypted queries, making topic-level correlation infeasible even for a fully malicious provider.

## Ciphertext Length Analysis

On-chain ciphertext lengths are visible to everyone. Without mitigation, message length alone can reveal information (e.g. distinguishing a "yes" from a paragraph). See [here](../../how-it-works/ratcheting.md#ciphertext-padding) how Verbeth pads all plaintext into fixed-size buckets before encryption.
62 changes: 62 additions & 0 deletions apps/docs/docs/how-it-works/message-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
sidebar_position: 5
title: Message Store
---

# Message Store

The SDK does not prescribe a storage backend. It defines two interfaces that applications implement to connect `VerbethClient` to whatever persistence layer they choose. This page explains what those interfaces require and where the boundary sits between SDK logic and app logic.

## SessionStore

The `SessionStore` interface (`client/types.ts`) has three methods.

```typescript
interface SessionStore {
get(conversationId: string): Promise<RatchetSession | null>;
getByInboundTopic(topic: string): Promise<RatchetSession | null>;
save(session: RatchetSession): Promise<void>;
}
```

`get()` retrieves a session by its primary key, which is the `conversationId` (a `keccak256` hash of the sorted topic pair).

`getByInboundTopic()` is the critical query. When an on-chain message event arrives, the SDK knows only the topic. It needs to find the corresponding session. This lookup is the app's responsibility. The SDK's internal `SessionManager` (`client/SessionManager.ts`) provides caching and topic promotion logic, but it delegates the actual database query to this method.

The store must be able to find a session where the given topic matches any of `currentTopicInbound`, `nextTopicInbound`, or `previousTopicInbound`. This typically means indexing the session table on all three fields.

`save()` persists the session state after every encrypt or decrypt operation. Failing to persist means the ratchet can roll back to stale state, which breaks forward secrecy and can create duplicate message keys.

## PendingStore

The `PendingStore` interface (`client/types.ts`) manages the lifecycle of outbound messages.

```typescript
interface PendingStore {
save(pending: PendingMessage): Promise<void>;
get(id: string): Promise<PendingMessage | null>;
getByTxHash(txHash: string): Promise<PendingMessage | null>;
updateStatus(id: string, status: PendingStatus, txHash?: string): Promise<void>;
delete(id: string): Promise<void>;
getByConversation(conversationId: string): Promise<PendingMessage[]>;
}
```

A `PendingMessage` moves through two active states: `preparing` (created before tx submission) and `submitted` (tx broadcast, txHash known). On confirmation the record is deleted, so there is no persistent `confirmed` state. On failure, `markFailed()` handles submit-time errors (tx never broadcast), while `revertTx()` handles post-broadcast reverts, so both remove the pending record. The `getByTxHash` index allows matching on-chain events back to pending records. See [VerbethClient](./verbeth-client.md) for the two-phase commit pattern that uses this store.

## Serialization

`RatchetSession` contains binary fields: `rootKey`, `dhMySecretKey`, `dhMyPublicKey`, `dhTheirPublicKey`, `sendingChainKey`, `receivingChainKey`, and the `messageKey` inside each `SkippedKey` entry. These are all `Uint8Array` values that most storage backends cannot persist directly.

Applications must handle the conversion between binary arrays and whatever format their database supports. Common approaches include base64 encoding for IndexedDB, hex encoding for SQL, or binary columns where available. The important thing is that the round-trip is lossless and that sensitive fields (root key, chain keys, DH secret keys, message keys) are treated with the same care as any cryptographic secret.


## Ephemeral state

Some state lives outside the two store interfaces and requires separate attention.

**Pending handshakes.** Between sending a handshake and receiving its response, the ephemeral secret key and the KEM secret key must be stored somewhere. The SDK returns them from `sendHandshake()` and expects them back when `createInitiatorSession()` or `createInitiatorSessionFromHsr()` is called. The app is responsible for persisting these across sessions. In an app build with Verbeth, they live in the `pendingHandshakes` table alongside the contact address and timestamp.

**Session cache.** `SessionManager` maintains an in-memory `Map<string, RatchetSession>` that avoids repeated database reads for the same session. The cache is invalidated when `invalidateSessionCache()` is called, or cleared entirely with `clearSessionCache()`. This cache is ephemeral and rebuilt on demand.

**Topic transition windows.** The `previousTopicInbound` and its expiry timestamp are part of the persisted `RatchetSession`, not ephemeral. But the decision to listen on that topic is made by the `SessionManager` during topic lookup. The store just needs to return sessions where any of the three topic fields match.
Loading
Loading