================================================================================ 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 // 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.