166 lines
7.9 KiB
ReStructuredText
166 lines
7.9 KiB
ReStructuredText
================================================================================
|
||
Normal Mode – Complete Detailed Design & Persistence Specification
|
||
================================================================================
|
||
|
||
**Project**: World's Most Secure Messaging App
|
||
**Mode**: Normal Mode (default for all channels)
|
||
**Date**: February 20, 2026
|
||
**Version**: 1.0 – Written for someone who has never built a ratchet before
|
||
|
||
This document explains **everything** from first principles. Every term is defined the first time it appears. Every high-level statement is followed immediately by the low-level cryptographic details so you can understand exactly how it works and implement it.
|
||
|
||
|
||
|
||
1. What “Ratchet” Actually Means (Simple Explanation)
|
||
=====================================================
|
||
|
||
A **ratchet** is a security system that works like a real metal ratchet tool you use on a socket wrench:
|
||
|
||
- It only turns **one way** (forward).
|
||
- Every time you click it forward, the old position is **permanently lost** — you cannot turn it backward.
|
||
- This gives **forward secrecy**: if someone steals your keys tomorrow, they cannot go back and read messages you sent yesterday.
|
||
|
||
In our app, the ratchet is a mathematical way to keep changing the encryption keys for every single message.
|
||
|
||
|
||
|
||
2. What the Double Ratchet Actually Is (High-Level + Low-Level)
|
||
===============================================================
|
||
|
||
We use a **Double Ratchet**.
|
||
It has two separate chains that run at the same time.
|
||
|
||
**High-Level View**
|
||
- Chain 1 (Symmetric Ratchet): Advances on almost every message. Very fast.
|
||
- Chain 2 (Asymmetric Ratchet): Runs less often (you decide how often). This is the strong “reset” step that gives post-compromise security.
|
||
|
||
**Low-Level View – Symmetric Ratchet (the one that runs every message)**
|
||
|
||
Start with a 32-byte secret called the **chain key**.
|
||
|
||
For every new message:
|
||
1. Take the current chain key.
|
||
2. Run it through your chosen KDF (e.g. KMAC256) with the data “next” + groupId.
|
||
3. The output becomes the **new chain key** for the next message.
|
||
4. The old chain key is thrown away forever (deleted from memory and never stored).
|
||
5. From the **current** chain key you also derive the actual encryption key for this message (called the **base message key**):
|
||
|
||
```text
|
||
base_message_key = KDF(chain_key, counter.to_bytes(8) + groupId, output=32 bytes)
|
||
```
|
||
|
||
This is the “click” of the ratchet. Each message moves the chain forward and deletes the previous state.
|
||
|
||
**Low-Level View – Asymmetric Ratchet Step (the “reset” you asked about)**
|
||
|
||
This is the part that “injects fresh randomness and resets the symmetric chain”.
|
||
|
||
How it works in detail:
|
||
|
||
1. The app decides it is time for a reset (according to your setting in CCCData.ratchetFrequency, e.g. every 50 messages).
|
||
|
||
2. The sender generates a fresh public-key keypair using the KEM algorithm you chose in CCCData.kemConfigs (for example X448 + Kyber1024 + McEliece460896).
|
||
|
||
3. The sender sends the **public** part of this keypair **inside** the encrypted payload of the current message (never in the wire header).
|
||
|
||
4. The receiver decrypts the message normally, sees the new public key, and performs the KEM decapsulation to get a fresh shared secret (32–64 bytes of true randomness).
|
||
|
||
5. Both sides now take this fresh shared secret and mix it into the symmetric chain:
|
||
|
||
```text
|
||
new_chain_key = KDF(old_chain_key, fresh_shared_secret, "asymmetric-reset" + groupId)
|
||
```
|
||
|
||
6. The old symmetric chain key is deleted forever.
|
||
|
||
This is the “reset”.
|
||
It injects **brand new, high-entropy randomness** that the attacker does not have, even if they had stolen all previous keys.
|
||
After this step, the conversation “heals” — past compromises no longer matter for future messages.
|
||
|
||
This is exactly how post-compromise security works.
|
||
|
||
|
||
|
||
3. What Exactly Is Stored on Your Device (Very Specific)
|
||
========================================================
|
||
|
||
For each chat (channel) we store exactly these things:
|
||
|
||
- **CCCData** – one object per channel containing every setting you chose (which providers, which KEM, which AEADs, ratchet frequency, padding size, etc.).
|
||
|
||
- **Raw encrypted wire blobs** – for every message that ever arrived, we store the **exact bytes** that came from the relay. These are the permanent chat history. They are stored in Hive exactly as received.
|
||
|
||
- **Ratchet state** (one set per person you are chatting with in that channel):
|
||
- encryptedRootKey (the original shared secret, encrypted under your device master key)
|
||
- currentChainKey (the latest symmetric chain key, 32 bytes)
|
||
- lastProcessedCounter (the highest message counter we have successfully decrypted for this person)
|
||
- checkpoints (a small map: every 500 messages we save a snapshot of the chain key at that counter so we don’t have to replay 10,000 steps when loading very old messages)
|
||
|
||
We **never** store a key for each individual message.
|
||
We only keep the current state of the ratchet plus the raw encrypted blobs.
|
||
|
||
|
||
|
||
4. Message Flow From One User to Another (Detailed)
|
||
===================================================
|
||
|
||
**Sending Side (User A → User B)**
|
||
|
||
1. User A types a message.
|
||
2. App loads CCCData for this channel (all your chosen algorithms).
|
||
3. App loads User A’s sending ratchet state for this channel.
|
||
4. App advances the symmetric chain (runs KDF on currentChainKey to produce newChainKey).
|
||
5. App derives base_message_key = KDF(newChainKey, currentCounter + 1, groupId).
|
||
6. App pads the plaintext using the block size and random padding bytes you set in CCCData.
|
||
7. App encrypts the padded plaintext using the list of AEAD algorithms in CCCData (round-robin).
|
||
8. App puts the ciphertext into RelayMessage.data.
|
||
9. App sends the RelayMessage to the relay.
|
||
10. App updates its sending ratchet state (saves newChainKey and increments counter) and stores the raw RelayMessage locally.
|
||
|
||
**Receiving Side (User B receives)**
|
||
|
||
1. User B receives RelayMessage from relay.
|
||
2. App immediately stores the exact raw bytes in Hive.
|
||
3. App loads CCCData and User A’s receiving ratchet state.
|
||
4. App advances the receiving chain forward until it reaches the incoming message counter.
|
||
5. App derives the same base_message_key.
|
||
6. App decrypts using the same AEAD list from CCCData.
|
||
7. App removes padding and shows the message.
|
||
8. App updates and saves the receiving ratchet state.
|
||
|
||
|
||
|
||
5. How We Load Messages on Cold Start
|
||
=====================================
|
||
|
||
**Recent Messages (what you see immediately)**
|
||
|
||
1. You open the app and authenticate → device master key is created.
|
||
2. App loads CCCData and the current ratchet state for each person.
|
||
3. App loads the newest raw blobs from storage.
|
||
4. For each new message, it advances the ratchet forward from the last saved state, derives the base message key, decrypts, and shows it.
|
||
5. After decrypting, it updates the saved ratchet state.
|
||
|
||
**Older Messages (when you scroll up)**
|
||
|
||
1. You scroll up.
|
||
2. Older messages appear grayed out with “Tap to unlock older messages”.
|
||
3. When you tap, the app asks for your passphrase or biometrics again.
|
||
4. If successful, the app replays the ratchet forward from the last saved state to the old messages you want to see.
|
||
5. It derives the correct base message key for each old message, decrypts the raw blob, and shows it.
|
||
6. It updates the ratchet state and checkpoints so next time it is faster.
|
||
|
||
|
||
|
||
6. How the Ratchet Chain Is Started (Channel Creation / Invite)
|
||
===============================================================
|
||
|
||
1. Creator generates a fresh 32-byte root secret.
|
||
2. Root secret is encrypted under the recipient’s public key (using the KEM you chose in CCCData) and sent in the invite.
|
||
3. Recipient decrypts the root secret.
|
||
4. Both sides run the same initialization:
|
||
- sendingChainKey = KDF(rootSecret, "sending" + groupId)
|
||
- receivingChainKey = KDF(rootSecret, "receiving" + groupId)
|
||
5. Both save the encrypted root and initial chain keys.
|
||
6. Counters start at 0.
|