222 lines
7.9 KiB
ReStructuredText
222 lines
7.9 KiB
ReStructuredText
================================================================================
|
||
Normal Mode – Full Ratchet Design & Persistence (Modular Pseudo-Code)
|
||
================================================================================
|
||
|
||
Modular Design Overview (Reusable Components)
|
||
=============================================
|
||
|
||
- `CCCData` → user-configurable settings (KDF, AEAD list, ratchetFrequency, etc.)
|
||
- `RatchetEngine` → main class, one instance per channel
|
||
- `ChainState` → per-sender state (sending + receiving)
|
||
- `KeyDerivation` → static functions for all KDF calls
|
||
- `MessageStore` → Hive wrapper for raw blobs + checkpoints
|
||
|
||
|
||
|
||
Message Flow From One User to Another (End-to-End)
|
||
==================================================
|
||
|
||
**Sender Side (User A sends to User B)**
|
||
|
||
```pseudo
|
||
function sendMessage(channel, plaintext):
|
||
ccc = loadCCCData(channel.groupId)
|
||
state = loadChainState(channel.groupId, myDeviceId) // my sending state
|
||
|
||
// 1. Advance symmetric ratchet if configured
|
||
if (ccc.symmetricRatchetEveryMessage):
|
||
state.sendingChainKey = KDF(ccc.kdf, state.sendingChainKey, "next" + groupId)
|
||
|
||
// 2. Generate base_message_key for this exact message
|
||
baseKey = deriveMessageKey(state.sendingChainKey, state.lastSentCounter + 1, ccc)
|
||
|
||
// 3. Pad plaintext
|
||
padded = pad(plaintext, ccc.blockSize, ccc.minPaddingBytes, baseKey)
|
||
|
||
// 4. Encrypt with round-robin AEADs from CCCData
|
||
ciphertext = roundRobinAEAD(padded, baseKey, ccc.aeadList)
|
||
|
||
// 5. Build RelayMessage
|
||
relayMsg = new RelayMessage()
|
||
relayMsg.from_rly_user_id = myRlyUserId
|
||
relayMsg.to_rly_chan_id = channel.to_rly_chan_id
|
||
relayMsg.data = ciphertext
|
||
relayMsg.senderCounter = state.lastSentCounter + 1
|
||
|
||
// 6. Send via gRPC
|
||
gRPC.send(relayMsg)
|
||
|
||
// 7. Update and save state
|
||
state.lastSentCounter += 1
|
||
saveChainState(state)
|
||
|
||
// 8. Store raw message locally for history
|
||
storeRawMessage(channel.groupId, myDeviceId, relayMsg.senderCounter, relayMsg)
|
||
```
|
||
|
||
**Receiver Side (User B receives)**
|
||
|
||
```pseudo
|
||
function onReceive(relayMsg):
|
||
ccc = loadCCCData(relayMsg.to_rly_chan_id)
|
||
state = loadChainState(relayMsg.to_rly_chan_id, relayMsg.from_rly_user_id) // receiving state
|
||
|
||
// 1. Advance receiving chain to match senderCounter
|
||
while (state.lastReceivedCounter < relayMsg.senderCounter):
|
||
if (ccc.symmetricRatchetEveryMessage):
|
||
state.receivingChainKey = KDF(ccc.kdf, state.receivingChainKey, "next" + groupId)
|
||
state.lastReceivedCounter += 1
|
||
|
||
// 2. Derive base_message_key
|
||
baseKey = deriveMessageKey(state.receivingChainKey, relayMsg.senderCounter, ccc)
|
||
|
||
// 3. Decrypt
|
||
padded = roundRobinAEADDecrypt(relayMsg.data, baseKey, ccc.aeadList)
|
||
plaintext = removePadding(padded)
|
||
|
||
// 4. Store raw + update state
|
||
storeRawMessage(groupId, relayMsg.from_rly_user_id, relayMsg.senderCounter, relayMsg)
|
||
saveChainState(state)
|
||
|
||
return plaintext
|
||
```
|
||
|
||
2. How We Generate a Ratchet Key (Modular Function)
|
||
===================================================
|
||
|
||
```pseudo
|
||
function deriveMessageKey(chainKey: bytes32, counter: uint64, ccc: CCCData) -> bytes32:
|
||
// Exact algorithm used for every message key
|
||
info = counter.toBigEndianBytes(8) + groupId
|
||
return KDF(ccc.kdfAlgorithm, chainKey, info, outputLength=32)
|
||
|
||
function KDF(kdfType, key, info, outputLength):
|
||
if (kdfType == "KMAC256"):
|
||
return KMAC256(key=key, data=info, length=outputLength)
|
||
if (kdfType == "HKDF-SHA512"):
|
||
return HKDF_SHA512(ikm=key, info=info, length=outputLength)
|
||
// add more as user chooses in CCCData
|
||
```
|
||
|
||
3. What Key Material We Store Per Channel (No Per-Message Keys)
|
||
===============================================================
|
||
|
||
We store **only 4 things** per sender per channel:
|
||
|
||
- `rootKey` (initial shared secret, 32 bytes, AES-256-GCM encrypted under deviceMasterKey)
|
||
- `sendingChainKey` (32 bytes, current)
|
||
- `receivingChainKey` (32 bytes, current)
|
||
- `lastSentCounter` and `lastReceivedCounter` (uint64)
|
||
|
||
Plus optional checkpoints (your "index keys" request):
|
||
|
||
```pseudo
|
||
class ChainState:
|
||
encryptedRootKey // AES-256-GCM(rootKey, deviceMasterKey)
|
||
sendingChainKey // 32 bytes (plain in RAM, encrypted on disk if you want)
|
||
receivingChainKey // 32 bytes
|
||
lastSentCounter // uint64
|
||
lastReceivedCounter // uint64
|
||
|
||
// Checkpoints - every 500 messages (your "few index keys")
|
||
checkpoints: Map<uint64, bytes32> // counter -> chainKey at that point
|
||
// e.g. key 500, 1000, 1500...
|
||
```
|
||
|
||
We never store per-message keys.
|
||
Checkpoints allow fast jump to any old message without replaying the entire history.
|
||
|
||
4. How We Load Previous Messages and Decrypt Them
|
||
==================================================
|
||
|
||
**Loading Recent Messages (fast path)**
|
||
|
||
```pseudo
|
||
function loadRecentMessages(groupId, limit=100):
|
||
messages = queryHiveMessages(groupId, newest first, limit)
|
||
for each msg in messages:
|
||
if (msg.senderCounter > state.lastReceivedCounter):
|
||
// replay forward from current state
|
||
state = replayForward(state, msg.senderCounter, ccc)
|
||
baseKey = deriveMessageKey(state.receivingChainKey, msg.senderCounter, ccc)
|
||
plaintext = decryptRawMessage(msg.rawRelayMessage, baseKey, ccc)
|
||
cachePlaintext(msg, plaintext)
|
||
```
|
||
|
||
**Loading Older Messages (when user scrolls up)**
|
||
|
||
```pseudo
|
||
function decryptOlderMessages(groupId, targetCounter):
|
||
state = loadChainState(groupId, senderId)
|
||
|
||
// Find nearest checkpoint <= targetCounter
|
||
checkpointCounter = findNearestCheckpoint(state.checkpoints, targetCounter)
|
||
chainKey = state.checkpoints[checkpointCounter]
|
||
|
||
currentCounter = checkpointCounter
|
||
|
||
while (currentCounter < targetCounter):
|
||
if (ccc.symmetricRatchetEveryMessage):
|
||
chainKey = KDF(ccc.kdf, chainKey, "next" + groupId)
|
||
currentCounter += 1
|
||
|
||
baseKey = deriveMessageKey(chainKey, targetCounter, ccc)
|
||
|
||
// Decrypt the raw blob
|
||
plaintext = decryptRawMessage(rawRelayMessage, baseKey, ccc)
|
||
|
||
// Update checkpoint if needed
|
||
if (targetCounter % 500 == 0):
|
||
state.checkpoints[targetCounter] = chainKey
|
||
saveChainState(state)
|
||
|
||
return plaintext
|
||
```
|
||
|
||
5. How We Begin Generation of the Ratchet Key Chain From Root / Pub Key
|
||
=======================================================================
|
||
|
||
**At Channel Creation / Invite Acceptance**
|
||
|
||
```pseudo
|
||
function initializeRatchetFromRoot(rootKey: bytes32, ccc):
|
||
state = new ChainState()
|
||
|
||
state.encryptedRootKey = aes256GcmEncrypt(rootKey, deviceMasterKey)
|
||
|
||
// Initial chain keys from root
|
||
state.sendingChainKey = KDF(ccc.kdf, rootKey, "sending-chain" + groupId)
|
||
state.receivingChainKey = KDF(ccc.kdf, rootKey, "receiving-chain" + groupId)
|
||
|
||
state.lastSentCounter = 0
|
||
state.lastReceivedCounter = 0
|
||
|
||
// Create first checkpoint
|
||
state.checkpoints[0] = state.sendingChainKey
|
||
|
||
saveChainState(state)
|
||
```
|
||
|
||
6. What Key Material We Share in Invite Code Package
|
||
====================================================
|
||
|
||
**Invite Package (encrypted blob sent to new user)**
|
||
|
||
```pseudo
|
||
class InvitePackage:
|
||
cccData: CCCData // full user-chosen config
|
||
encryptedRootKey: bytes // rootKey encrypted under recipient's long-term public key
|
||
// (using recipient's KEM from CCCData.kemConfigs)
|
||
initialGroupId: bytes32
|
||
creatorDeviceId: bytes32
|
||
```
|
||
|
||
New user decrypts `encryptedRootKey` with their private key → calls `initializeRatchetFromRoot()` → both users now have identical ratchet starting state.
|
||
|
||
This is the complete, modular, detailed pseudo-code for Normal Mode.
|
||
|
||
You can now implement `RatchetEngine`, `ChainState`, `KeyDerivation`, and the storage classes directly.
|
||
|
||
Everything is reusable for Ultra Mode later.
|
||
|
||
Ready to code. Let me know when you want the Ultra Mode version in the same style. |