NEW: init commit

This commit is contained in:
JohnE 2026-02-23 02:03:57 -08:00
commit df1de9d99d
29 changed files with 5794 additions and 0 deletions

194
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,194 @@
# LetUsMsg Flutter App - AI Coding Agent Instructions
**Project**: LetUsMsg (Flutter cross-platform messaging app)
**Focus**: gRPC relay-based secure messaging with local Hive persistence
## Architecture Overview
### Core Components
1. **Relay Communication** (`flutter_src/lib/relay/`)
- `grpc_service.dart`: gRPC client managing streaming messages from relay servers and sending messages
- Handles message sequences, acknowledgments, and connection state
- Uses `fixnum.Int64` for protobuf compatibility (common error: forgetting `import 'package:fixnum/fixnum.dart'`)
- **Key workflow**: `registerClient()``streamMessages()``_procMessage()``ackMessages()`
2. **Data Layer** (`flutter_src/lib/data/`)
- Hive CE (encrypted local DB) for persistent storage
- Models: `MyselfData`, `ChannelData`, `MessageData`, `RelayPoolData`, `RelayStateData`, `CCCData` and more look at the path `flutter_src/lib/data/`
- All models use `@HiveType()` annotations; code generation required (`build_runner`)
- **Message storage**: Size-based approach (active box + archived boxes per channel)
3. **Processing Layer** (`flutter_src/lib/proc/`)
- `proc_main.dart`: Central orchestrator
- Abstract base classes: `ProcConnAbstract`, `ProcMsgAbstract`, `NetworkSendAbstract`
- Handles cryptography, message sending, incoming stream processing
- Runs message background work via `worker_manager` isolates
4. **State Management** (`flutter_src/lib/bloc/`)
- BLoC pattern (custom, not `flutter_bloc` package)
- `AppState`: Global singleton for `appNotify`, `conn`, `fcm`, `msgNotify`, `proc`
- `ConnectionStateUI`: Connection state per relay (connecting/registered/disconnected)
- Changes broadcast via callbacks: `StreamMsgCallback`, `StreamCloseCallback`, `StreamCountCallback`
5. **UI Layer** (`flutter_src/lib/`)
- `main.dart`: Multi-platform entry (macOS, iOS, Android, Linux, Windows)
- Platform detection: `Platform.isMacOS`, `Platform.isIOS`, etc.
- Custom widgets in `widgetz/`; theme via `flex_color_scheme`
## Key Workflows & Patterns
### Message Relay Flow
```
User sends → UI callback → NetworkSendAbstract.sendMessage()
→ gRPC stub.sendMessage(Message)
→ Relay server → other clients receive via streamMessages()
→ _procMessage() processes with sequence tracking
→ _lastMsgSeq == serverSeq → ackMessages(AckRequest)
→ Clear cached messages, reset _firstMsgSeq/_lastMsgSeq
```
### gRPC Type Handling (Critical)
- **Protobuf definitions**: `proto/letusmsg.proto` defines service `MessagingService`
- **Generated stubs**: `flutter_src/lib/gen/proto/letusmsg.pbgrpc.dart` (auto-generated)
- **Integer types**: Always use `Int64` (from `fixnum`) for protobuf `int64` fields
- Common error: `Int16(_lastMsgSeq)` → use `Int64(_lastMsgSeq)` instead
- Nullable fields: `Int64?` requires conditional: `_lastMsgSeq == -1 ? null : Int64(_lastMsgSeq)`
- **Method calls**: Use named parameters, e.g., `_stub.ackMessages(request: ackRequest, options: ...)`
### Hive Data Generation
- All Hive models require code generation
- **Build command**: `flutter pub run build_runner build --delete-conflicting-outputs`
- Files: `*_data.g.dart` (auto-generated adapters)
- Models must have: `@HiveType()`, `@HiveField(0)` annotations
- **Storage locations**: `dataDir` (from args or default app docs dir)
### Multi-Platform Desktop Window Management
- macOS/Windows/Linux: Use `window_manager` (see `main.dart`)
- Mobile (iOS/Android): Firebase initialization required
- Window options: Title, size (600x800 default), titlebar style, min/max dimensions
## Common Pitfalls & Solutions
### Notification Tap Handling (Critical for Mobile)
The app handles notifications from **three sources**:
1. **FCM (Firebase Cloud Messaging)** - Remote push notifications (`relay/fcm.dart`)
2. **flutter_local_notifications** - Local notifications (`notifications/local_notification.dart`)
3. **URI Deep Links** - Custom scheme `com.letusmsg://` via `app_links` package
**Three App States for Notification Taps:**
| State | FCM Handler | Local Notification Handler | URI Deep Link |
|-------|-------------|---------------------------|---------------|
| **Terminated (cold start)** | `getInitialMessage()` | `getNotificationAppLaunchDetails()` | `getInitialLink()` |
| **Background (warm start)** | `onMessageOpenedApp` stream | `onDidReceiveNotificationResponse` callback | `uriLinkStream` |
| **Foreground (hot)** | `onMessage` stream | `onDidReceiveNotificationResponse` callback | `uriLinkStream` |
**Key Pattern**: Always check for "initial message/link" on app startup because callbacks aren't registered when app is terminated. Store in `MyApp.pendingDeepLink` and process after splash screen via `MyApp.processPendingDeepLink()`.
**Deep Link Handler**: All notification taps route through `utilz/deep_link_handler.dart`:
- `DeepLinkArgs.fromUri()` - Parse URI deep links
- `DeepLinkArgs.fromFcmData()` - Parse FCM notification data payload
- `DeepLinkArgs.fromPayload()` - Parse local notification payload string
**FCM Data Payload Structure** (server sends this):
```json
{
"data": {
"channel": "abc123-channel-uuid"
}
}
```
**URI Deep Link Format**:
- Open channel: `com.letusmsg://chat?channel=abc123`
- Accept invite: `com.letusmsg://chat?invite-code=89ae57b9-4435-4956-b735-37bd5af00a5e`
**Flow for Cold Start from Notification**:
1. User taps notification → App launches
2. `FCM.init()` calls `getInitialMessage()` → stores in `initialMessageData` if callback not ready
3. `_setupNotificationHandlers()` checks `initialMessageData` → sets `MyApp.pendingDeepLink`
4. Splash finishes → `MyApp.processPendingDeepLink()``DeepLinkHandler.handle()` → navigate to channel
## Build & Test Commands
**Manual Flutter commands**:
```bash
flutter pub get # Install deps
flutter pub run build_runner build --delete-conflicting-outputs # Hive codegen
flutter run -v # Run with logs
flutter test # Run unit tests (test/)
```
## Project-Specific Dependencies
- **gRPC/Protobuf**: `grpc: ^4.1.0`, `fixnum: ^1.1.1` (strict version to avoid dependency issues)
- **State Management**: `provider: ^6.1.2` (lightweight, not flutter_bloc)
- **Local Storage**: `hive_ce: ^2.10.1` (encrypted), `path_provider: ^2.0.15`
- **Async**: `worker_manager: ^7.2.6` (for background isolates)
- **Firebase**: `firebase_core`, `firebase_messaging` (mobile only, see `main.dart`)
- **Notifications**: `flutter_local_notifications` (local notifications with cold start support)
- **Deep Linking**: `app_links: ^6.4.0` (URI scheme and universal links)
- **UI**: `flex_color_scheme` (dynamic theming), `flutter_svg` (SVG support)
- **Custom packages** (git-based):
- `lum_loading_animations` (internal)
- `linkify_text` (forked, improved regex)
## File Organization
```
flutter_src/
├── lib/
│ ├── main.dart # Entry point, platform detection
│ ├── app.dart # App widget
│ ├── bloc/ # State management
│ │ ├── app_state.dart # Global singleton
│ │ ├── conn_state_UI.dart # Connection per relay
│ │ └── ...
│ ├── data/ # Hive models & persistence
│ │ ├── myself_data.dart
│ │ ├── my_hive.dart # Hive box manager
│ │ └── ...
│ ├── notifications/ # Push & local notifications
│ │ ├── local_notification.dart # flutter_local_notifications wrapper
│ │ └── permissions.dart # Notification permission handling
│ ├── proc/ # Business logic & processing
│ │ ├── proc_main.dart
│ │ └── *_abstract.dart # Abstract interfaces
│ ├── relay/ # gRPC communication
│ │ ├── grpc_service.dart # Main gRPC client
│ │ └── fcm.dart # Firebase Cloud Messaging
│ ├── utilz/ # Utilities
│ │ ├── deep_link_handler.dart # Deep link & notification routing
│ │ └── Llog.dart # Logging configuration
│ ├── widgetz/ # UI widgets
│ └── ...
├── pubspec.yaml # Dependencies
└── ...
proto/
├── letusmsg.proto # Protobuf definitions
```
## Next Steps for Contributors
- **First-time setup**: Run `flutter pub get` and `flutter pub run build_runner build --delete-conflicting-outputs`
- **Understand gRPC service**: Read `relay/grpc_service.dart` (callbacks, state machine)
- **Understand meessage flow**: `proc/proc_bg_msg.dart` is the message processing engine
- **Hive persistence**: Review `data/my_hive.dart` and size-based message storage in `new_message_storage_architecture.md`
- **Adding features**: Use abstract base classes in `proc/` and follow BLoC pattern in `bloc/`
- **Testing**: Add tests to `test/` and run `flutter test`
## Notes
- **Logging**: Uses `logger` package; configure in `utilz/Llog.dart`
- **Crypto**: Phase 1 implemented via `cryptography: ^2.7.0`; see `proc/crypt_abstract.dart`
- **CCC** (Copius Cipher Chain): Internal message protocol; see `data/ccc_data.dart`
## Instructions for AI Coding Agent
- Follow the established architecture and coding patterns.
- his project is using dart lang and flutter framework. you are an elite coder who knows the dart lang design patterns and the flutter design patterns that are the most recent and popular, and you apply those with pricision, focusing on an amazing user experience, focusing on responsive design (because this app will run on mobile phones and desktop)
- make sure to follow the code and design patterns for this project, that includes file names, notifications, and popups and confirmation popups; there is an app constants and app theme constants that should be used and provided more details of the app design; do your research;
- this is the worlds most secure messaging app. the app is design so that the main isolate is dedicated to the UI and there is a background isolate that handles a lot of IO for the app, for example local storage with hive db, networking with grpc, and encryption.
- prioritize efficiency and clean code: avoid duplicate logic/UI; extract reusable widgets/helpers for repeated behavior so there is one place to maintain changes.
- DO NOT remove any existing features! you can ask me questions for more clarity on your solutions and fixes.
- please ask me any questions and share you design before changing code
- if you create documents use .rst format and name the files in lowercase
- if you create code files, follow the existing naming patterns and file organization
- if you create new features, make sure to add tests for them in the test/ directory and follow the existing testing patterns
- if you create new classes and methods(more than 5 lines of code) , make sure to add doc comments and follow the existing code style and patterns

15
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"cmake.configureOnOpen": false,
"dart.analysisExcludedFolders": [
"flutter_src",
"ex_src"
],
"search.exclude": {
"flutter_src": true,
},
"files.watcherExclude": {
"**/var/**": true,
"**/bin/**": true
},
"foldOnOpen.targets": ["AllBlockComments"]
}

View File

@ -0,0 +1,148 @@
================================================================================
Ultra Mode ASCII Diagrams (Hydra Apocalypse Ratchet)
================================================================================
These diagrams illustrate the core flows in Ultra Secret Mode.
All flows assume:
- CCCData is fully configured and shared at channel creation.
- No ratchet_public is ever sent on wire (pre-defined symmetric ratchet + rekey inside payload).
- Relay is stateless RAM forwarder.
- Everything except absolute minimum routing info is inside the encrypted `data` field.
1. Message Lifecycle (Full End-to-End Flow)
===========================================
.. code-block:: text
Sender Device (Ultra Mode enabled)
|
User types message + current timestamp
|
Load CCCData → build cascade pool (48-64 entries from enabled providers)
|
Advance inner Double Ratchet (symmetric every msg) → base_message_key
|
Derive seq_seed = KMAC256(base_message_key + "seq-seed" + counter)
|
Generate secret cascade sequence (16-24 layers) via CSPRNG(seed)
|
Pad plaintext: to block_size + random min_padding_bytes from KDF
|
Inner AEAD round-robin (user-chosen aeadConfigs) → inner_ct
|
Cascade encrypt (strongest first) → cascade_ct
|
If cascadeReKeyAfter: new_inner_root = KMAC256(cascade_ct + old_root)
|
Outer AEAD (fast fixed) on cascade_ct → final_data
|
Build RelayMessage:
from_rly_user_id | to_rly_chan_id | options | data (final_data) | server_seq
|
Send via gRPC to relay
|
v
Ephemeral Relay (RAM only no disk, no keys)
|
+------------------> Recipient Device A
| |
| Store raw RelayMessage to Hive immediately
| |
+------------------> Recipient Device B
| |
| Store raw RelayMessage to Hive immediately
|
v
Relay discards blob after forwarding
2. Rekey Flow (Post-Compromise Security Inside Payload)
=========================================================
.. code-block:: text
Sender decides to rekey (manual button / auto interval)
|
Generate fresh hybrid KEM keypair(s) using ccc.kemConfigs
|
Encapsulate new group_master_seed (or root secret) → shared_secret
|
Create RekeyPayload (JSON inside encrypted data):
{
"type": "rekey",
"new_root_secret": encapsulated_shared_secret,
"rekey_counter": current_sender_counter,
"timestamp": now
}
|
Proceed with normal message encryption:
- Use CURRENT base_message_key + cascade to encrypt the RekeyPayload + real message
|
Send as normal RelayMessage (recipient sees it as regular message)
|
Recipient decrypts normal flow:
|
Detects "type": "rekey" inside decrypted payload
|
Extracts and decapsulates new_root_secret
|
Updates group_master_seed / root keys / chain keys
|
All future messages use the new root → PCS achieved
3. Message Over the Wire (Minimal gRPC Payload Zero Metadata)
===============================================================
.. code-block:: text
+-------------------------------- RelayMessage (gRPC over TLS/Noise) -------------------------------+
| from_rly_user_id (4 B) | to_rly_chan_id (4 B) | options (4 B) | server_seq (8 B) | |
| data_length (4 B) | data (variable) |
| |
| data contents (E2EE never visible to relay): |
| timestamp (8 B) + message JSON + padding + RekeyPayload (if rekey) + inner ciphertext layers |
+-----------------------------------------------------------------------------------------------------+
→ Relay sees only routing IDs + opaque encrypted data
→ No public keys, no timestamps, no sequence beyond sender_counter (derived inside)
4. Loading Messages from Local Storage (Hive → UI)
==================================================
.. code-block:: text
User opens channel → app queries Hive 'messages' box
|
For each row (key = groupId + fromRlyUserId + senderCounter):
|
Load StoredMessage.rawRelayMessage (full protobuf bytes)
|
Extract RelayMessage.data from protobuf
|
Load CCCData for groupId
|
Advance receiving inner ratchet using fromRlyUserId + senderCounter
|
Derive base_message_key + seq_seed + cascade_sequence (deterministic)
|
Decrypt outer AEAD → cascade_ct
|
Cascade decrypt (reverse order, using same provider/algorithm selections)
|
Inner AEAD round-robin decrypt → padded_plaintext
|
Remove padding → extract timestamp + message content + (if present) RekeyPayload
|
If RekeyPayload → apply new root secrets
|
Display timestamp + content in UI
|
(Optional) Cache plaintext in StoredMessage.plaintext for faster reload

View File

@ -0,0 +1,201 @@
.. _worlds-most-secure-messaging-app-full-system-design:
====================================================================
World's Most Secure Messaging App Full System Design Specification
====================================================================
**Codename**: Hydra Apocalypse Ratchet (Ultra Secret mode)
**Author**: JohnΞ (@j3g)
**Date**: February 18, 2026
**Version**: 1.0 Complete implementation-ready specification
**Target**: Ephemeral-relay messaging app (messages exist only in RAM on relays)
Executive Summary
=================
This is the **complete, implementation-ready** end-to-end encryption system we finalized across our entire discussion.
- **Normal mode** (default for family/just-chat): fast hybrid PQ Double Ratchet.
- **Ultra Secret mode** (opt-in per group/chat): triple-compound ratchet with 1624 diverse vetted ciphers, per-group secret, per-message sequence mutation, and massive combinatorial + multi-math hedge.
The relay is **completely stateless and ephemeral** — it only forwards blobs in RAM and drops them. Nothing persistent, no keys, no state.
All design goals from our conversation are met: survive secret breaks in any single primitive, per-group/per-message secret sequences, dependent one-way keys, strongest-first ordering, padding against distinguishers, vetted libs only, FS + PCS preserved, quantum diversity.
Why This Design
===============
- Nation-state can secretly weaken one major algorithm (AES, Kyber, McEliece, etc.).
- We force them to break **many independent lineages simultaneously** while guessing a secret per-group sequence that mutates per message.
- Ephemeral relays eliminate server-side storage attacks.
- Local storage keeps raw wire messages so history can be re-decrypted on demand.
Core Architecture
=================
- **Ephemeral Relays**: Pure RAM forwarders. Receive blob from sender, forward identical blob to each recipient, then discard.
- **Device Local Storage**: SQLite (or equivalent) encrypted at rest with device master key (separate from E2EE). Stores **raw wire messages** (ciphertext + metadata) directly.
- **Ratchet State**: Per-group, per-sender (sending + receiving chains) stored encrypted at rest.
- **Two Modes**:
- Normal: Hybrid PQ Double Ratchet (fast).
- Ultra Secret: Hydra Apocalypse Ratchet (inner PQ Double Ratchet + massive secret cascade + sequence mutation).
Wire Format (What IS and IS NOT Sent to the Relay)
===================================================
**IS sent over the wire to the relay (and forwarded identically to recipients)**:
- ``group_id`` (256-bit)
- ``sender_id`` (256-bit device/user identifier)
- ``sender_counter`` (64-bit monotonically increasing integer per sender per group)
- ``ratchet_public`` (optional KEM public key / DH public key when performing a DH/KEM ratchet step — ~12 KB)
- ``payload`` (the final E2EE ciphertext blob — see below)
- ``timestamp`` (for ordering/display, not security)
**IS NOT sent** (never on wire, never to relay):
- Any keys, seeds, sequence seeds, cascade permutations, inner keys, base_message_key, etc.
- Plaintext
- Any long-term secrets
The relay sees only routing metadata + opaque encrypted payload. It forwards the exact same blob to each members device (via push or long-poll). TLS/Noise is used for the device↔relay transport link, but that is **separate** from E2EE.
Normal Mode Ratchet (Detailed)
==============================
- Initial group setup: Hybrid KEM (X448 + Kyber-1024 + Classic McEliece 460896 round-robin) to establish per-sender root keys.
- Per sender per group: independent Double Ratchet (symmetric HKDF chain + hybrid KEM ratchet every 3080 messages).
- Per message: derive base_message_key from current sending chain.
- AEAD: round-robin AES-256-GCM / ChaCha20-Poly1305 / Ascon-AEAD-128a using base_message_key.
- On receive: advance receiving chain using sender_counter and optional ratchet_public.
- FS/PCS: standard Double Ratchet deletion of old chain keys.
Ultra Secret Mode Ratchet (The Full Monster Detailed)
=======================================================
**Per-group setup** (at group creation, threshold-shared):
- ``group_master_seed`` (512-bit)
**Per-sender initialization** (each member generates for itself and shares public parts):
- ``sending_root_key`` = HKDF(group_master_seed, "sender-root-" || sender_id)
- ``receiving_root_key`` (per other sender) = derived during first message exchange
**For every outgoing message (sender_counter)**:
1. Advance the **inner Double Ratchet** to produce ``base_message_key`` (256-bit).
2. Derive cascade material deterministically from base_message_key:
- ``seq_seed`` = KMAC256(base_message_key, b"seq-seed-" || group_id || sender_counter)
- ``cascade_permutation`` = deterministic_permutation_from_seed(seq_seed, pool_size=4864, length=1624)
3. Inner AEAD encrypt plaintext → inner_ct (triple AEAD with base_message_key).
4. Cascade encrypt inner_ct using the 1624 layers (strongest first):
- Per-layer key = HKDF-Expand(base_message_key, b"layer-" || layer_index || group_id || sender_counter)
- Apply sequentially (encrypt outer-to-inner or as defined).
5. (Optional) Re-key the inner ratchet root with final cascade output for extra mixing.
6. Produce final payload = outer AEAD (Ascon or AES-GCM) on the cascade ciphertext for wire integrity.
**Sequence mutation** (Layer C): The seq_seed itself is derived fresh per sender_counter → every message has a completely different secret permutation.
**On receive** (using sender_id + sender_counter):
- Advance receiving Double Ratchet to derive the exact same ``base_message_key``.
- Re-derive seq_seed and cascade_permutation (deterministic).
- Decrypt cascade in reverse, then inner AEAD.
Local Storage Exact Details
=============================
**What is stored directly from the wire** (raw, unchanged):
- Full received blob:
- group_id, sender_id, sender_counter, ratchet_public (if present), timestamp
- payload (the exact E2EE ciphertext received)
Stored in local DB table ``messages`` (encrypted at rest with device master key):
.. code-block:: sql
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
group_id BLOB,
sender_id BLOB,
sender_counter INTEGER,
timestamp INTEGER,
payload BLOB, -- raw wire ciphertext
base_message_key BLOB, -- encrypted with device master key (see below)
plaintext BLOB NULL -- optional: decrypted on first view
);
**How we figure out the keys to decrypt from local storage**:
1. Load row by group_id + sender_id + sender_counter.
2. Decrypt the stored ``base_message_key`` using the device master key.
3. Using that base_message_key + group_id + sender_counter:
- Re-derive seq_seed = KMAC256(base_message_key, b"seq-seed-..." )
- Re-derive exact cascade permutation and all 1624 per-layer keys.
- Decrypt cascade in reverse order.
- Decrypt inner triple AEAD.
4. (Optional) Cache plaintext in the same row for future views.
**Forward Secrecy Note**:
- The E2EE ratchet deletes old chain keys after successful receive/send (standard FS).
- Local storage keeps the base_message_key (encrypted under device key) so history remains decryptable.
- If the **device** is later compromised, history is readable — this is the explicit trade-off for on-demand decryption of stored wire messages. The cascade still provides the multi-alg hedge even if base_message_key leaks.
Message Sending Flow (Ultra Secret)
===================================
1. User types message.
2. Pad plaintext (multiple of 64 bytes + 3264 random HKDF bytes from base_message_key).
3. Run ultra_encrypt as above.
4. Build wire blob with metadata + payload.
5. Send to relay (over TLS/Noise) with recipient list or group_id.
Message Receiving Flow (Ultra Secret)
=====================================
1. Receive blob from relay.
2. Store raw blob + encrypted base_message_key to local DB immediately.
3. Derive base_message_key via receiving ratchet (using sender_counter).
4. Decrypt using the process above.
5. Display plaintext.
6. Advance all ratchets (inner + sequence is automatic via counter).
Pseudocode Ultra Secret Core Functions
========================================
.. code-block:: python
def derive_base_message_key(state, sender_counter):
# Standard Double Ratchet advance (symmetric + optional KEM)
return advance_double_ratchet(state.receiving_chain[sender_id], sender_counter)
def get_cascade_for_message(base_message_key, group_id, sender_counter):
seq_seed = kmac256(base_message_key, b"seq-seed-" + group_id + sender_counter.to_bytes(8))
permutation = deterministic_permutation(seq_seed, pool=64, length=20) # 16-24
return permutation
def ultra_decrypt(wire_payload, base_message_key, group_id, sender_counter):
seq = get_cascade_for_message(base_message_key, group_id, sender_counter)
ct = wire_payload # after outer AEAD verify if present
# decrypt cascade reverse
for layer in reversed(seq):
ct = decrypt_layer(ct, derive_layer_key(base_message_key, layer, sender_counter))
plaintext = inner_triple_aead_decrypt(ct, base_message_key)
return plaintext
def store_received_message(blob):
base_key = derive_base_message_key(...) # from ratchet
encrypted_base_key = aes_gcm_encrypt(base_key, device_master_key)
db.insert(group_id=blob.group_id, sender_id=blob.sender_id,
sender_counter=blob.sender_counter,
payload=blob.payload,
base_message_key=encrypted_base_key)
Implementation Priorities (Start Here)
======================================
1. Normal mode first (Double Ratchet + PQ KEM).
2. Per-sender ratchet tables.
3. Local DB schema exactly as above.
4. Ultra mode cascade dispatcher (test with 4 layers first).
5. Deterministic permutation function (ChaCha20 seeded by seq_seed).
6. Device master key management (separate from E2EE).
7. Threshold group creation for group_master_seed.

View File

@ -0,0 +1,291 @@
================================================================================
Normal Mode Encryption Specification Hydra Apocalypse Ratchet
================================================================================
**Project**: World's Most Secure Messaging App
**Mode**: Normal Mode (default for all channels)
**Codename**: Hydra Apocalypse Ratchet Normal Mode
**Author**: JohnΞ (@j3g)
**Date**: February 18, 2026
**Version**: 1.1 Dart/Flutter + Hive + Modular CCCData + Encrypted Channel Blob (last phase)
**Primary Design Goal (Non-Negotiable)**
=======================================
This app is built to be **the most secure messaging application on the planet**.
Even in Normal Mode (the fast default), we refuse to make the compromises that Signal and every other mainstream app make.
The user has **full cryptographic sovereignty**: they choose every primitive, every provider (wolfSSL, BoringSSL, libsodium, OpenSSL, etc.), every parameter.
The entire scheme is defined at channel creation time and never changed by the server or developer.
Executive Summary
=================
Normal Mode is the **default encryption scheme** for every channel.
It is a **highly configurable, post-quantum hybrid Double Ratchet** that is deliberately stronger than Signal's 2026 PQ Triple Ratchet because:
- Every cryptographic primitive and provider is user-selected at channel creation.
- The complete configuration lives in **CCCData** (modular, provider-aware).
- All parameters are stored locally in Hive and shared securely during channel invite.
- Full forward secrecy + post-compromise security with user-controlled rekey frequency.
- Zero server influence over cryptography.
CCCData is the **single source of truth** for the encryption scheme.
CCCData Modular & Provider-Aware (Dart)
==========================================
.. code-block:: dart
@HiveType(typeId: 42)
class CCCData extends HiveObject {
@HiveField(0)
final int version = 1;
@HiveField(1)
final String mode = 'normal'; // 'normal' or 'ultra'
// Modular crypto providers (user can enable any combination)
@HiveField(2)
final List<String> enabledProviders; // e.g. ['wolfssl', 'boringssl', 'libsodium', 'openssl']
// Category-based configuration user picks per category, with provider fallback
@HiveField(3)
final Map<String, List<CryptoSelection>> kemConfigs; // key = category, value = ordered list (round-robin)
@HiveField(4)
final Map<String, List<CryptoSelection>> aeadConfigs;
@HiveField(5)
final Map<String, List<CryptoSelection>> kdfConfigs;
@HiveField(6)
final Map<String, List<CryptoSelection>> hashConfigs;
// Ratchet behavior (user configurable)
@HiveField(7)
final int ratchetFrequency; // messages between full KEM ratchet steps (0 = every message)
@HiveField(8)
final bool symmetricRatchetEveryMessage;
@HiveField(9)
final int rekeyInterval; // 0 = manual only
@HiveField(10)
final String rekeyMode; // 'manual', 'auto', 'button'
// Anti-distinguisher / padding
@HiveField(11)
final int minPaddingBytes;
@HiveField(12)
final int blockSize; // pad to multiple of this
// Channel identifiers
@HiveField(13)
final Uint8List groupId; // 32 bytes
@HiveField(14)
final int createdAt; // unix ms
@HiveField(15)
final Uint8List creatorDeviceId; // 32 bytes
CCCData({
required this.enabledProviders,
required this.kemConfigs,
required this.aeadConfigs,
required this.kdfConfigs,
required this.hashConfigs,
required this.ratchetFrequency,
required this.symmetricRatchetEveryMessage,
required this.rekeyInterval,
required this.rekeyMode,
required this.minPaddingBytes,
required this.blockSize,
required this.groupId,
required this.createdAt,
required this.creatorDeviceId,
});
}
@HiveType(typeId: )
class CryptoSelection extends HiveObject {
@HiveField(0)
final String provider; // 'wolfssl', 'boringssl', etc.
@HiveField(1)
final String algorithm; // 'Kyber1024', 'AES256GCM', 'KMAC256', etc.
@HiveField(2)
final int priority; // 0 = first in round-robin, higher = later
}
Defaults (hard-coded best-in-class):
- enabledProviders: ['wolfssl', 'boringssl', 'libsodium']
- kemConfigs['hybrid']: [wolfssl:X448+Kyber1024+McEliece460896, boringssl:Kyber1024, libsodium:X25519+Kyber768]
- aeadConfigs['main']: [wolfssl:ChaCha20-Poly1305, boringssl:AES256GCM, libsodium:Ascon-AEAD-128a]
All fields are **user-editable** in the channel creation screen (with defaults pre-selected and highlighted).
Channel Creation & Sharing (Current + Future Phase)
===================================================
**Current Phase**:
1. User creates channel → app builds CCCData with user choices.
2. Generate group_master_seed (512-bit).
3. Store CCCData + group_master_seed in Hive (encrypted with device master key).
4. Invite link contains encrypted (under recipients public key) copy of CCCData + group_master_seed.
**Future Phase Encrypted Channel Blob (Last Phase)**:
- When sharing a channel, serialize the entire CCCData + group_master_seed + initial ratchet states into a single encrypted blob (using a strong symmetric key derived from invite secret).
- This blob is the only thing sent to new members.
- Signature algorithm
Local Persistence Hive
========================
All data uses Hive:
- Box 'cccdata' → key = groupId.toHex(), value = CCCData
- Box 'messages' → composite key = '${groupId.toHex()}_${fromRlyUserId}_${senderCounter}'
- Model:
```dart
@HiveType(typeId: 50)
class StoredMessage extends HiveObject {
@HiveField(0) Uint8List groupId;
@HiveField(1) int fromRlyUserId;
@HiveField(2) int senderCounter;
@HiveField(3) Uint8List rawRelayMessage; // entire protobuf
@HiveField(4) Uint8List encryptedBaseKey; // encrypted with device master key
@HiveField(5) int timestamp; // extracted after decrypt
@HiveField(6) String? plaintext; // optional cache
}
```
Wire Protocol (RelayMessage) Backwards Compatible
===================================================
.. code-block:: proto
message RelayMessage {
uint32 from_rly_user_id = 1;
uint32 to_rly_chan_id = 2;
uint32 options = 3; // kept for backwards compatibility (will be removed later)
bytes data = 4; // FULL E2EE ciphertext (inner timestamp + content + padding + rekey if any)
uint64 server_seq = 5; // kept for backwards compatibility (will be removed later)
}
**data** field contains everything E2EE (timestamp for UI, message JSON, rekey payload, padding, etc.).
Normal Mode Ratchet Suggested Implementation
==============================================
```dart
class NormalRatchet {
final CCCData ccc;
final Uint8List groupMasterSeed;
// Per-sender chains
Map<int, Uint8List> sendingChainKeys = {}; // fromRlyUserId -> current chain key
Map<int, Uint8List> receivingChainKeys = {};
Uint8List advanceAndGetBaseKey(int fromRlyUserId, int senderCounter, bool isSending) {
var chain = isSending ? sendingChainKeys[fromRlyUserId]! : receivingChainKeys[fromRlyUserId]!;
// Symmetric ratchet every message (user configurable)
if (ccc.symmetricRatchetEveryMessage) {
chain = KDF(ccc.kdfConfigs, chain, utf8.encode('next'));
}
// Derive base_message_key
final baseKey = KDF(ccc.kdfConfigs, chain, senderCounter.toBytes());
// Update chain for next message
if (isSending) sendingChainKeys[fromRlyUserId] = chain;
return baseKey;
}
// Full KEM ratchet step (triggered by ratchetFrequency)
Future<void> performKEMRatchet(int fromRlyUserId) async {
final kemPub = await HybridKEM.generate(ccc.kemConfigs); // uses user-chosen providers
final sharedSecret = await HybridKEM.encap(...); // encapsulated inside encrypted payload
// Update root and chains
}
}
```
Message Sending Flow (Normal Mode)
==================================
1. Load CCCData for channel.
2. Advance ratchet → base_message_key.
3. Pad plaintext per CCCData.blockSize + minPaddingBytes (random bytes from KDF).
4. Round-robin encrypt with AEADs from ccc.aeadConfigs using base_message_key.
5. If rekey triggered → add RekeyPayload inside inner data.
6. Serialize into RelayMessage.data.
7. Send RelayMessage to relay.
Message Receiving & Local Storage Flow
======================================
1. Receive RelayMessage → immediately store raw bytes in Hive 'messages' box.
2. Load CCCData → advance receiving ratchet → derive base_message_key.
3. Decrypt data field → extract timestamp for UI.
4. If RekeyPayload → apply new root.
5. Cache plaintext if desired.
Rekey Flow
==========
- auto every rekeyInterval messages
- Fresh hybrid KEM secret encapsulated and sent inside encrypted data (never on wire header).
Security Guarantees Why This Is the Most Secure Normal Mode on Earth
=====================================================================
- User chooses every provider and algorithm (wolfSSL AES + BoringSSL Kyber + libsodium Ascon in same channel).
- All decisions cryptographically bound to channel via CCCData.
- Modular provider system allows mixing libraries for maximum implementation diversity.
- No ratchet_public on wire (future-proofed).
- Hive at-rest encryption + device master key protects everything locally.
Implementation Checklist (Dart/Flutter)
=======================================
1. Hive adapters for CCCData, CryptoSelection, StoredMessage.
2. Channel creation screen with provider picker + category lists.
3. CryptoProvider registry (wolfssl.dart, boringssl.dart, etc.) with abstract interfaces.
4. NormalRatchet class that reads CCCData at runtime.
5. HybridKEM, AEADRoundRobin, KDF engines that respect provider lists.
6. Invite flow that sends encrypted CCCData blob.
7. Unit tests for every possible CCCData combination.
8. Fuzz padding, ratchet advance, provider fallback.
Future Phase Encrypted Channel Blob
=====================================
- Serialize entire CCCData + groupMasterSeed + initial ratchet states.
- Encrypt under a strong symmetric key derived from invite secret.
- This single blob becomes the canonical way to share a channel (last phase).

View File

@ -0,0 +1,165 @@
================================================================================
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 (3264 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 dont 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 As 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 As 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 recipients 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.

View File

@ -0,0 +1,222 @@
================================================================================
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.

View File

@ -0,0 +1,849 @@
====================================================================
LetUsMsg Encryption Architecture Phased Implementation Plan
====================================================================
:Project: LetUsMsg
:Date: 2026-02-20
:Status: Phase 0 COMPLETED · Phase 1 COMPLETED · Phase 2 COMPLETED
:Codename: Hydra Apocalypse Ratchet
:Reference: ``encryption_implementation_normal-ccc.rst``, ``encryption_implementation_ultra-ccc.rst``
Phase Progress Status
=====================
.. list-table::
:header-rows: 1
:widths: 10 40 15
* - Phase
- Title
- Status
* - 0
- Security Baseline Hardening
- **COMPLETED**
* - 1
- Normal Mode v1 (Classical + Hybrid-Ready)
- **COMPLETED**
* - 2
- Ratchet Core (Forward Secrecy / Post-Compromise Security)
- **COMPLETED**
* - 3
- Full Normal Mode Target Parity
- Not started
* - 4
- Native Crypto Provider (wolfSSL FFI + Capability Matrix)
- Not started
* - 5
- Ultra Secret Mode (Triple-Compound Ratchet)
- Not started
* - 6
- Encrypted Channel Blob + Invite Crypto
- Not started
* - 7
- Verification, Audit, and Deployment Hardening
- Not started
Executive Summary
=================
This document is the single implementation-facing plan for the LetUsMsg
Hydra Apocalypse Ratchet encryption system. The full cryptographic
specifications for each mode live in dedicated companion documents:
- **Normal mode** (default): see ``encryption_implementation_normal-ccc.rst``
- **Ultra Secret mode** (opt-in): see ``encryption_implementation_ultra-ccc.rst``
The target architecture is **dual-mode per channel**:
- **Normal mode**: highly configurable, post-quantum hybrid Double Ratchet.
User-chosen primitives and providers. Fast, highly secure, PQ-ready
migration path. Stronger than Signal's 2026 PQ Triple Ratchet by design.
- **Ultra Secret mode**: Triple-Compound Ratchet (inner hybrid ratchet +
massive 1624 layer secret cascade + per-message sequence mutation).
Maximum defense-in-depth for threat models that assume nation-state
primitive compromise.
Both modes share the **CCCData** model as the single source of truth for
per-channel encryption configuration.
Threat model:
- Possible hidden weaknesses / backdoors in individual primitives.
- Harvest-now-decrypt-later quantum risk.
- Targeted high-budget cryptanalysis.
- Chosen / known-plaintext amplification in messaging systems.
Design Principles
=================
- **Defense in depth**: multiple independent primitive families where practical.
- **Forward secrecy + post-compromise security** via ratcheting.
- **Per-group uniqueness** and **per-message mutation**.
- **Dependent one-way key derivation** (HKDF / KMAC family).
- **Conservative ordering** in cascade layers.
- **User cryptographic sovereignty**: every primitive, provider, and parameter
is user-selected at channel creation time through CCCData.
- **Length-hiding padding**:
- Normal mode: minimal overhead (1632 bytes).
- Ultra mode: pad to 64-byte boundary + 3264 bytes random-derived padding.
- **Ephemeral relay compatibility**: larger ciphertext overhead is acceptable
given the stateless-RAM relay architecture.
- **Background-isolate execution**: all crypto / network / storage runs off
the main UI isolate (``worker_manager`` pool).
What Exists Now (update according to progress)
==============================================
* Current: (Post Phase 0 + Phase 1 + Phase 2 Progress)
CCC Infrastructure (20 files in ``flutter_src/lib/ccc/``)
----------------------------------------------------------
.. list-table::
:header-rows: 1
:widths: 35 65
* - File
- Purpose
* - ``crypto_abstract.dart``
- ``CryptoAbstract<T>`` interface, ``CryptoContext``, ``CryptoProviderMode`` enum
* - ``crypto_ccc.dart``
- ``CryptoCcc`` production provider backed by ``CryptoIsolateManager``
* - ``crypto_plaintext.dart``
- ``CryptoPlaintext`` testing/dev-only plaintext alias (blocked in prod)
* - ``crypto_wolfssl.dart``
- ``CryptoWolfSsl`` scaffold only, throws ``UnimplementedError`` (Phase 4)
* - ``ccc_iso_manager.dart``
- ``CryptoIsolateManager`` 4-worker pool, encrypt/decrypt dispatch, metrics
* - ``ccc_iso.dart``
- Isolate worker with 6 implemented primitives (see below)
* - ``ccc_iso_operation.dart``
- ``CryptoOperation`` value object (channel, sequence, cipher, priority)
* - ``ccc_iso_operation_id.dart``
- Sequence-based fast operation ID generator
* - ``ccc_iso_result.dart``
- ``CryptoResult`` + ``CryptoMetrics`` (timing, throughput, error rate)
* - ``ccc_key_schedule.dart``
- ``CccKeySchedule`` Phase 1 deterministic root-key + per-message key derivation
* - ``ccc_channel_profile.dart``
- ``ChannelCccProfile`` per-channel combo/iteration/mode resolution + route planning
* - ``ccc_provider_spec.dart``
- ``CccProviderCatalog``, ``CccCipherCapability``, execution-mode routing
* - ``ccc_kdf.dart``
- ``CccKdfFunction`` enum (SHA-256/384/512, BLAKE2b-512) + normalization
* - ``cipher_constants.dart``
- 35+ cipher constants, ``PHASE1_SEQUENCE``, ``BASIC_*_SEQUENCE``, ``DUAL_AEAD_SEQUENCE``,
``TRIPLE_AEAD_SEQUENCE``, ``COMBO_NAMES``, ``COMBO_SEQUENCES``
* - ``copious_cipher_chain.dart``
- ``CopiousCipherChain`` high-level facade with round-trip testing
* - ``attachment_crypto_context.dart``
- Lightweight context carrier for attachment encrypt/decrypt
* - ``enc_dec_json.dart``
- ``EncDecJson`` plaintext JSON codec retained for testing
* - ``ratchet_engine.dart``
- ``RatchetEngine`` symmetric ratchet core: init, advance, checkpoint, old-message derivation
* - ``ratchet_state_manager.dart``
- ``RatchetStateManager`` Hive CRUD for ``RatchetState``, channel-level cleanup
Implemented Primitives (via ``cryptography`` package)
-----------------------------------------------------
All implemented in the isolate worker (``ccc_iso.dart``):
- AES-256-GCM (AEAD)
- ChaCha20-Poly1305 (AEAD)
- XChaCha20-Poly1305 (AEAD, 24-byte nonce)
- HMAC-SHA512 (authentication layer)
- BLAKE2b (integrity hash)
- Argon2id (key derivation, reserved not in default Phase 1 chain)
Default ``PHASE1_SEQUENCE`` (5 layers, combo 0):
``AES-256-GCM → ChaCha20-Poly1305 → XChaCha20-Poly1305 → HMAC-SHA512 → BLAKE2b``
User-selectable combos:
- **Combo 5** Basic: AES-256-GCM (single AEAD)
- **Combo 6** Basic: ChaCha20-Poly1305 (single AEAD)
- **Combo 7** Basic: XChaCha20-Poly1305 (single AEAD, 24-byte nonce)
- **Combo 8** Dual AEAD: AES-256-GCM + ChaCha20-Poly1305
- **Combo 9** Triple AEAD: AES + ChaCha20 + XChaCha20
Derived-Key Encryption (Real E2E)
---------------------------------
All AEAD / HMAC layers now support **derived-key mode** when the key
schedule parameter ``phase1_root_key_b64`` is present in cipher params.
Key derivation formula per layer::
layer_key = SHA-512(root_key_bytes || "|lk|{layerIndex}|c|{cipherConstant}"),
truncated to cipher's key length (32 B for AEADs, 64 B for HMAC)
Output format comparison:
.. list-table::
:header-rows: 1
:widths: 20 40 40
* - Layer Type
- Legacy (random key in blob)
- Derived-key (real E2E)
* - AEAD (AES/ChaCha/XChaCha)
- ``[32B key][nonce][ct][16B MAC]``
- ``[nonce][ct][16B MAC]``
* - HMAC-SHA512
- ``[64B key][data][64B MAC]``
- ``[data][64B MAC]``
* - BLAKE2b
- ``[data][64B hash]`` (keyless)
- ``[data][64B hash]`` (unchanged)
Size savings for standard 5-layer combo: **160 bytes per message**
(3×32 B AEAD keys + 64 B HMAC key no longer embedded).
Legacy mode (random keys embedded) is retained for backwards compatibility
when ``phase1_root_key_b64`` is absent in params.
AEAD Associated Data
--------------------
All AEAD layers bind canonicalized associated data including:
channel UUID, KDF function, CCC combo, CCC iterations.
Phase 1 exclusions (both stripped by the isolate worker before AEAD binding):
- **direction**: sender uses ``'out'``, receiver uses ``'in'`` for the same
ciphertext, so it cannot be bound.
- **message_sequence**: the sender encrypts before the relay assigns
``server_seq``, so the receiver's relay sequence would mismatch. Sequence
binding will be re-enabled in Phase 2 when the ratchet provides
agreed-upon per-message counters.
Message + Attachment Pipeline Integration
-----------------------------------------
- ``ProcBgMsg`` selects ``CryptoCcc`` or ``CryptoPlaintext`` at runtime.
- Production builds reject plaintext mode.
- ``_buildCryptoContext()`` resolves channel → ``ChannelCccProfile````CccKeySchedule.buildMessageParams()`` → full ``CryptoContext``.
- Attachment paths in ``proc_bg_actions.dart`` use the same ``ChannelCccProfile`` + ``CccKeySchedule`` pipeline.
Provider Routing Infrastructure
--------------------------------
- 4 providers defined: ``cryptography`` (available), ``wolfssl`` / ``openssl`` / ``boringssl`` (scaffold, ``available: false``).
- 3 execution modes: ``strict`` (no fallback), ``efficient`` (ranked by score), ``auto`` (capability-based).
- 10 combo patterns (09) in ``ChannelCccProfile`` mapping to different
provider/cipher combinations. Combos 14 are multi-provider scaffolds;
combos 59 are user-selectable single/dual/triple AEAD configurations.
- Iteration expansion (116x repetition of cipher sequence).
Test Suites (11 files, 82 tests)
--------------------------------
.. list-table::
:header-rows: 1
:widths: 35 65
* - Test File
- Coverage
* - ``ccc_phase0_security_test.dart``
- Ciphertext ≠ plaintext, tamper-fail, nonce uniqueness (legacy mode)
* - ``ccc_key_schedule_test.dart``
- Deterministic keys, sequence variation, KDF selection, CCCData round-trip
* - ``ccc_channel_profile_test.dart``
- Default profiles, combo-specific mapping (09), iteration clamping/expansion, modes,
unknown combo fallback
* - ``ccc_associated_data_test.dart``
- AD tampering, KDF mismatch, channel mismatch, combo mismatch, Phase 1 sequence tolerance
* - ``ccc_e2e_roundtrip_test.dart``
- Sender→receiver roundtrip (default + custom CCC), cross-channel rejection,
empty/large payloads, nonce uniqueness, tamper rejection, all 4 KDFs, combo mismatch
* - ``ccc_derived_key_test.dart``
- Derived-key ciphertext size savings, all combos 59 roundtrip, minimal overhead
(combo 5), root-key isolation between channels, iterated derived-key roundtrip,
cross-combo rejection, tamper detection, large payload triple AEAD, empty payload,
combo metadata consistency
* - ``ccc_ratchet_engine_test.dart``
- RatchetEngine determinism, forward-only chain, checkpoint accuracy, old-message
derivation, KDF function selection, RatchetState data model, asymmetric stub (28 tests)
* - ``ccc_ratchet_roundtrip_test.dart``
- Phase 2 ratchet E2E roundtrip: single/multi-message, all single-cipher combos,
multi-layer combos, large payload (11 tests)
* - ``ccc_ratchet_3party_test.dart``
- 3-party group N-party state model: chain isolation, cross-sender misuse detection,
receiver agreement, interleaved multi-message, independent counters (10 tests)
* - ``ccc_ratchet_state_test.dart``
- RatchetState data model, Hive serialization round-trip, empty/large counters,
multi-state box, overwrite semantics (9 tests)
* - ``ccc_ratchet_state_manager_test.dart``
- RatchetStateManager CRUD: open/close, save/load, exists, delete, deleteChannel,
listForChannel, lazy reopen, key isolation (17 tests)
Data Model (``CCCData`` in Hive)
---------------------------------
Current persisted fields:
- ``combo`` (int) cipher chain combination pattern
- ``iterrations`` (int) encryption rounds
- ``executionMode`` (String) strict / efficient / auto
- ``kdfFunction`` (int) KDF selector (1=SHA-256, 2=SHA-384, 3=SHA-512, 4=BLAKE2b-512)
- ``ratchetFrequency`` (int) asymmetric ratchet trigger cadence (default 50)
- ``symmetricRatchetEveryMessage`` (bool) advance chain per message (default true)
- ``rekeyInterval`` (int) full rekey interval (reserved, default 0)
- ``checkpointInterval`` (int) checkpoint every N messages (default 500)
Reserved non-persisted fields: ``passHash``, ``comboHash``, ``pubpvt``, ``pub``
(all ``Uint8List?``, for future key-exchange integration).
What Is Not Yet Implemented
---------------------------
- Asymmetric (KEM) ratchet activation (stub in place, Phase 3).
- PQ KEM integration (ML-KEM / Kyber, Classic McEliece, BIKE / HQC).
- X448 key exchange.
- Ascon-AEAD-128a.
- KMAC256 plumbing in key schedule.
- ``dart:ffi`` native crypto bridge (wolfSSL / BoringSSL / libsodium).
- Full modular CCCData with user-selectable providers / algorithms (per Normal-CCC spec).
- Encrypted channel blob for invite sharing.
- Ultra Secret mode cascade / sequence ratchet.
- Length-hiding padding per ``CCCData.blockSize`` + ``minPaddingBytes``.
- Channel creation UI for crypto sovereignty settings.
Phased Implementation Plan
==========================
Phase 0 Security Baseline Hardening: **COMPLETED**
-----------------------------------------------------
Objectives:
- Replace encode/decode placeholder usage in message path.
- Keep plaintext mode available for development/testing only.
- Introduce cryptographic interface boundaries for provider swap.
Deliverables (all completed):
- ``EncDecJson`` retained as plaintext/testing implementation.
- Legacy ``passthroughMode`` removed from ``CryptoIsolateManager``.
- ``CryptoAbstract`` provider boundary with ``CryptoPlaintext``, ``CryptoCcc``,
and ``CryptoWolfSsl`` scaffold.
- ``ChannelCccProfile`` for per-channel CCC plan with provider chain + cipher route.
- Channel-derived CCC context threaded through message + attachment encrypt/decrypt.
- Baseline security tests (ciphertext ≠ plaintext, tamper-fail, nonce uniqueness).
Phase 1 Normal Mode v1 (Classical + Hybrid-Ready): **COMPLETED**
---------------------------------------------------------------------
Objectives:
- Deliver production-safe Normal mode with current available primitives.
- Integrate message encryption + attachment encryption under a consistent
channel/message key schedule.
- Enforce AEAD associated-data binding for replay/tamper protection.
Deliverables:
- [x] ``CccKeySchedule`` deterministic channel root-key derivation +
per-message key material derivation with selectable KDF function.
- [x] ``ProcBgMsg`` crypto context generation wired to ``ChannelCccProfile`` +
``CccKeySchedule.buildMessageParams()``.
- [x] Unified BG attachment crypto context in ``proc_bg_actions``
attachment read/write paths use key-schedule-enriched params.
- [x] AEAD associated-data binding enforced in isolate worker layers
(AES-GCM / ChaCha20-Poly1305 / XChaCha20-Poly1305).
- [x] Negative security guards passing:
- decrypt fails when associated data is modified,
- decrypt fails when ``kdfFunction`` changes,
- decrypt fails when channel context changes,
- decrypt fails when ``ccc_combo`` changes.
Phase 1 note: ``message_sequence`` and ``direction`` are excluded from
AEAD binding because the sender cannot know the relay-assigned
``server_seq`` at encryption time, and direction differs between
sender/receiver. Sequence binding will be re-enabled in Phase 2
when the ratchet provides agreed-upon counters.
- [x] Production builds reject plaintext mode at runtime.
- [x] E2E roundtrip verification: sender encrypt → receiver decrypt
succeeds across default and custom CCCData, all 4 KDFs, empty and
large payloads (``ccc_e2e_roundtrip_test.dart``, 9 tests).
- [x] All outbound/inbound messages encrypted/decrypted through worker crypto
(key-schedule integration verified end-to-end).
- [x] **Derived-key encryption** AEAD/HMAC layers derive per-layer keys
from ``phase1_root_key_b64`` via ``SHA-512(rootKey || domain-label)``.
Keys are never embedded in ciphertext (real E2E: only channel members
who compute the same root key can decrypt).
- [x] **Combo 5-9 (Basic through Triple AEAD)** user-selectable single,
dual, and triple AEAD configurations for crypto sovereignty.
- [x] Constant-time MAC/hash verification (``_verifyMacBytes``).
Exit Criteria:
- All outbound/inbound messages encrypted/decrypted through worker crypto.
- Attachments + message payloads use one coherent key derivation model.
- No pass-through encryption mode in production builds.
Remaining Work: **ALL DONE Phase 1 CLOSED 2026-02-19**
- [x] **Live relay E2E smoke test** ``test_manual/ccc_relay_e2e_test.dart``
confirmed: combo 0 (228 B) and combo 8 Dual-AEAD (284 B) encrypt → relay →
decrypt both passed against a real localhost relay2 gRPC instance.
- [x] ``_runtimeProviderMode`` flipped to ``CryptoProviderMode.ccc``
(``proc_bg_msg.dart``). Combo 0 channels continue as plaintext pass-through
at the cipher layer for backwards compatibility.
- [x] ``crypto_mode_guard`` restored to ``isDeveloperMode`` gate.
Phase 2 Ratchet Core (Forward Secrecy / Post-Compromise Security): **COMPLETED**
-------------------------------------------------------------------------------------
:Reference: ``normal_mode_ratchet_implementation.rst``
Objectives:
- Implement the symmetric Double Ratchet for per-message forward secrecy.
- Add ``RatchetState`` persistence model to Hive for per-channel per-sender
chain keys and counters.
- Integrate ratchet key derivation into the existing ``ProcBgMsg``
``CryptoCcc`` → isolate worker pipeline (replacing Phase 1 static keys).
- Maintain full backwards compatibility with pre-ratchet channels.
- Provide checkpoint-based random-access decryption for old messages.
- Build infrastructure for asymmetric (KEM) ratchet step (activated Phase 3).
Implementation Notes:
- The isolate worker (``ccc_iso.dart``) requires **zero changes** — it already
derives per-layer keys from a root key. The ratchet simply provides a
different root key for each message.
- ``RatchetEngine`` is a pure-logic class with static methods that operate
on ``RatchetState`` + ``CCCData``. It runs on the background isolate.
- Sender counter (``sc``) is embedded inside the encrypted payload — invisible
to relay, authenticated by AEAD.
- Checkpoints every 500 messages (configurable) allow O(500) chain replay
instead of O(N) from genesis when decrypting old messages.
Deliverables:
- [x] ``RatchetState`` Hive model (typeId 18) with chain keys, counters,
checkpoints, channel/sender identifiers.
- [x] ``RatchetEngine`` core: ``initializeFromRoot()``, ``advanceSending()``,
``advanceReceiving()``, ``deriveKeyForOldMessage()``, KDF helpers.
- [x] ``RatchetStateManager``: Hive box lifecycle, CRUD, channel deletion
cleanup.
- [x] ``CCCData`` extended with ratchet fields: ``ratchetFrequency``,
``symmetricRatchetEveryMessage``, ``rekeyInterval``, ``checkpointInterval``.
- [x] ``ProcBgMsg`` integration: ratchet-derived keys replace static
``CccKeySchedule`` keys in encrypt/decrypt path.
- [x] Sender counter (``sc``) field in wire payload (inside encrypted data)
+ ``sender_counter`` proto field on ``Message``/``MessageOne``.
- [x] Channel lifecycle hooks: ratchet init on create/invite-accept,
cleanup on channel delete, lazy init for unknown senders on receive.
- [x] Asymmetric ratchet stub (``performAsymmetricRatchet()`` no-op, called
at configured frequency, ``rk`` payload field reserved for Phase 3).
- [x] ``MsgDataOutCache`` schema extended with ``senderCounter`` +
``senderUuid`` for offline retry ratchet metadata preservation.
- [x] Test suite: ``ccc_ratchet_state_test.dart``,
``ccc_ratchet_state_manager_test.dart``, ``ccc_ratchet_engine_test.dart``,
``ccc_ratchet_roundtrip_test.dart``, ``ccc_ratchet_3party_test.dart``.
Remaining Work: **ALL DONE Phase 2 CLOSED**
Exit Criteria:
- Every message uses a unique ratchet-derived key (no two messages share the
same base_message_key).
- Forward secrecy verified: advancing the chain and deleting old keys means
past messages cannot be derived from current state.
- Checkpoint-based old-message decryption works within 500-step replay bound.
- Pre-ratchet channels fall back to Phase 1 static key schedule without errors.
- All existing Phase 0/1 tests continue to pass.
- New ratchet test suite passes (determinism, forward-only, E2E round-trip).
- Sender counter survives round-trip through encrypt → relay → decrypt.
Phase 3 Full Normal Mode Target Parity
-----------------------------------------
Objectives:
- Reach the full Normal-mode design target from the Normal-CCC spec.
- Deliver user-facing crypto sovereignty (provider/algorithm selection UI).
- Complete Normal mode using the existing ``cryptography`` Dart package (no FFI required).
**Phase 3a Implemented (current sprint)**:
- **Expanded CCCData model** — 6 new HiveFields (813):
- ``rekeyMode`` (HF 8, String, default ``'manual'``).
- ``minPaddingBytes`` (HF 9, int, default 32).
- ``blockSize`` (HF 10, int, default 256).
- ``groupId`` (HF 11, ``List<int>?``, nullable for legacy).
- ``createdAt`` (HF 12, int, default 0).
- ``creatorDeviceId`` (HF 13, ``List<int>?``, nullable).
- ``fromMap()``, ``toMap()``, ``blank()``, ``equals()`` all updated.
- Hive codegen regenerated.
- **Length-hiding padding** in ``ccc_iso.dart``:
- ``_applyPadding(plaintext, params)``: pads to next multiple of
``blockSize`` with at least ``minPaddingBytes`` random bytes.
Format: ``[plaintext][random_padding][4-byte LE padding_length]``.
- ``_stripPadding(paddedData)``: reads trailer, validates, strips.
Falls back gracefully for pre-Phase-3 legacy data.
- Wired through ``ChannelCccProfile.fromCccData()`` → isolate params.
- 16 tests (unit + E2E integration) all passing.
- **Asymmetric ratchet activation** — X25519 via ``cryptography`` package:
- ``RatchetState`` expanded with DH key fields (HiveFields 811):
``dhPrivateKey``, ``dhPublicKey``, ``remoteDhPublicKey``,
``lastAsymmetricRatchetCounter``.
- ``RatchetEngine.performAsymmetricRatchet()``: generates fresh X25519
keypair, mixes public-key entropy into chain via KDF
(``KDF(chainKey || pubKeyBytes, "asymmetric-reset|channelUuid")``).
Group-compatible: all receivers apply same public key bytes.
- ``RatchetEngine.applyRemoteAsymmetricRatchet()``: receive-side method
that applies the same chain reset from the payload ``rk`` field.
- ``RatchetAdvanceResult.asymmetricRatchetPublicKey``: 32-byte X25519
public key returned when asymmetric step fires (null otherwise).
- ``advanceSending()`` wired to capture and propagate the public key.
- DH private key stored for potential Phase 4 pairwise DH enhancement.
- 12 dedicated asymmetric ratchet tests (unit + E2E) all passing.
- All 53 existing ratchet tests continue to pass.
**Phase 3b Remaining (future sprints)**:
- ``CryptoSelection`` Hive model (provider + algorithm + priority).
- ``enabledProviders``: user-selected list.
Per-category configs: ``kemConfigs``, ``aeadConfigs``, ``kdfConfigs``, ``hashConfigs``.
- Hybrid KEM/DH integration (target: X448 + ML-KEM-1024 Kyber, when pure-Dart packages available).
- Ascon-AEAD-128a added to negotiated AEAD profile (deferred — no pure-Dart impl exists).
- Round-robin AEAD encryption using ``aeadConfigs`` from CCCData.
- Channel creation UI:
- Provider picker + category lists for KEM, AEAD, KDF, Hash.
- Defaults pre-selected and highlighted (best-in-class).
- Advanced mode toggle for power users.
- Ratchet cadence + negotiation policy finalized and versioned.
- ProcBgMsg integration: embed ``rk`` field in encrypted payload on send,
extract and call ``applyRemoteAsymmetricRatchet()`` on receive.
Exit Criteria:
- Interop tests pass across supported device matrix.
- Performance / battery within defined budget.
- Channel creation with custom CCCData works end-to-end.
- Confirm message sequence tracking survives connection drops and app restart
(live device testing against relay server).
- Edge-case testing with real relay: out-of-order delivery, duplicate messages.
Phase 4 Native Crypto Provider (wolfSSL FFI + Capability Matrix)
------------------------------------------------------------------
Objectives:
- Add native provider to unlock full target primitive set and stronger
cross-platform guarantees.
- Enable multi-provider diversity (user can mix wolfSSL + BoringSSL +
libsodium in the same channel per CCCData configuration).
Deliverables:
- ``dart:ffi`` bridge package (workspace local module):
- Platform builds: iOS, macOS, Android, Linux, Windows.
- Deterministic API surface for AEAD / KDF / KEM.
- Secure memory handling and zeroization APIs.
- Runtime self-test and capability reporting.
- Provider implementations:
- ``CryptoWolfSsl`` preferred when capabilities available.
- ``CryptoCcc`` (``cryptography`` package) fallback / transitional.
- Additional provider scaffolds: ``CryptoBoringssl``, ``CryptoLibsodium``.
- ``CccProviderCatalog`` populated with real capability metadata:
- ``available``: runtime/provider readiness per platform.
- ``deterministicIo``: identical I/O contract flag for safe cross-provider fallback.
- ``efficiencyScore`` / ``reliabilityScore``: populated from benchmark suite
(``normalize(throughputMBps / latencyMs)`` , ``normalize((1 - errorRate) * 100)``).
- Initial PQ-ready integration:
- ML-KEM path stubs wired to key agreement flow.
Exit Criteria:
- Provider conformance tests pass identical test vectors across providers.
- Crash-safe fallback path works when native backend unavailable.
- At least two providers pass full AEAD + KDF test vector suite.
Phase 5 Ultra Secret Mode (Triple-Compound Ratchet)
------------------------------------------------------
Objectives:
- Implement the Hydra Apocalypse Ratchet Ultra Mode as specified in
``encryption_implementation_ultra-ccc.rst``.
- Deliver Layers A/B/C with controlled rollout and strict guardrails.
Deliverables:
- **CCCData Ultra-specific extensions** (per Ultra-CCC spec):
- ``cascadeLength`` (1624, default 20).
- ``cascadePoolSize`` (4864, default 56).
- ``cascadeOrdering`` (``'strongest_first'``, ``'random'``, ``'user_defined'``).
- ``cascadeReKeyAfter`` (re-key inner root after cascade, default true).
- ``sequenceRatchetStrength`` (bits for permutation seed, default 256).
- **Layer A Inner PQ Hybrid Ratchet**:
- Hybrid KEM: X448 + ML-KEM-1024 + Classic McEliece profile.
- Inner AEAD stack: AES-GCM + ChaCha20-Poly1305 + Ascon (round-robin from ``aeadConfigs``).
- Aggressive ratchet cadence: every 2050 messages.
- **Layer B Massive Secret Cascade**:
- Build cascade pool: 4864 unique ``(provider, algorithm)`` pairs from
all ``enabledProviders`` across symmetric, AEAD, PQ-KEM, PQ-signature, hash categories.
- Per-group secret permutation: select 1624 layers from pool.
- Per-layer key derivation:
.. code-block:: text
layer_key_i = HKDF-Expand(
prk = current_root_key,
info = "cascade-layer-" || group_id || sequence_index_i || epoch
)
- ``cascadeEncrypt()`` / ``cascadeDecrypt()`` dispatcher.
- **Layer C Sequence Ratchet**:
- Per-message sequence seed mutation (deterministic, never sent on wire):
.. code-block:: text
seq_seed = KMAC256(base_key, "seq-seed" || group_id || sender_counter)
cascade_sequence = deterministicPermutation(seq_seed, pool_size, length)
- Fisher-Yates shuffle with ChaCha20-seeded CSPRNG.
- Optional inner root re-key after cascade: ``KMAC256(cascade_ct + old_root)``.
- **UltraRatchet** class with full cascade pool builder + deterministic permutation.
- Ultra Mode message flow:
1. Load CCCData + build cascade pool.
2. Advance inner ratchet → ``base_message_key`` + ``seqSeed``.
3. Generate secret cascade sequence (1624 layers).
4. Pad plaintext (``blockSize`` boundary + ``minPaddingBytes`` random from KDF).
5. Inner AEAD round-robin on padded plaintext → ``innerCt``.
6. Cascade encrypt ``innerCt````cascadeCt``.
7. Optional inner root re-key.
8. Outer AEAD on ``cascadeCt````finalData``.
9. Build ``RelayMessage`` → send via gRPC.
- UI/UX:
- Ultra toggle in channel creation + advanced cascade settings tabs.
- Battery/performance warning indicators.
- Security mode clarity (lock icon / visual differentiation).
Exit Criteria:
- Fuzzing + adversarial test suite for cascade dispatcher / sequence engine.
- Regression suite validates decrypt reliability under reordering / loss.
- 4-layer cascade tested first, then scaled to full 20-layer.
- Performance benchmarking on real devices with battery impact documented.
Phase 6 Encrypted Channel Blob + Invite Crypto
-------------------------------------------------
Objectives:
- Secure channel sharing via a single encrypted blob containing all crypto state.
- Eliminate any plaintext crypto metadata in invite flow.
Deliverables:
- Serialize entire ``CCCData`` + ``groupMasterSeed`` (512-bit) + initial ratchet
states into a single encrypted blob.
- Encrypt blob under a strong symmetric key derived from invite secret.
- Invite link contains only the encrypted blob (under recipient's public key).
- Recipient imports → verifies ``groupId`` → stores in Hive → both devices have
identical crypto config.
- Signature algorithm for blob integrity verification.
Exit Criteria:
- Invite round-trip works across all platforms.
- Blob tampering detected and rejected.
- No crypto metadata leaked outside the encrypted blob.
Phase 7 Verification, Audit, and Deployment Hardening
--------------------------------------------------------
Objectives:
- Reduce implementation-risk surface and prepare for external review.
Deliverables:
- Differential fuzzing of encrypt/decrypt and ratchet transitions.
- Property-based tests (no key/nonce reuse, deterministic state transitions).
- Independent code review checklist + cryptographic design review packet.
- Release gates tied to security metrics and failure budgets.
- Performance/battery profiling for Normal vs Ultra modes.
- Zeroization verification (sensitive key material cleared from memory).
Exit Criteria:
- External audit report with no critical findings.
- All property-based invariants hold under fuzz testing.
- Release criteria met for target platforms.
CCC Provider-Chain Model
========================
CCC is the channel-level crypto plan and state, not a single crypto provider.
- Each channel stores CCC data (inside ``ChannelData.ccc``).
- CCC derives one-or-more providers for the channel plan (e.g.
``cryptography``, ``wolfssl``, ``openssl``, ``boringssl``).
- Each provider declares which ciphers it contributes.
- The channel profile flattens this provider chain into a concrete cipher
route for execution.
Execution Modes
---------------
- ``strict``:
- Follows exact provider order declared by the channel CCC plan.
- No fallback allowed.
- ``efficient``:
- Prefers best-known efficient provider route from configured providers.
- Fallback allowed when equivalent deterministic cipher I/O exists.
- ``auto``:
- Dynamically picks route based on provider capability metadata.
- Prefers currently available providers first.
- Fallback allowed when equivalent deterministic cipher I/O exists.
Fallback Policy
---------------
- Allowed only in ``efficient`` and ``auto`` modes.
- Disabled in ``strict`` mode.
- Rule: equivalent ciphers (e.g. AES-GCM) must preserve identical input/output
contract across providers to permit fallback.
Capability Metadata (``CccCipherCapability``)
---------------------------------------------
Per-provider/per-cipher metadata used by route planning:
- ``available``: runtime/provider readiness flag.
- ``deterministicIo``: identical I/O contract across providers (required for fallback).
- ``efficiencyScore`` (0100): performance/cost ranking (``efficient`` mode primary input).
- ``reliabilityScore`` (0100): stability/trust ranking (tie-break input).
Scores are placeholder (50) today. Benchmark suite will populate real values.
Implementation Notes and Constraints
=====================================
- Preserve current architecture: UI isolate remains responsive; crypto/network/storage
stay on background isolates (``worker_manager``).
- Keep relay protocol versioned for crypto profile negotiation and migration.
- ``RelayMessage.data`` carries all E2EE content (timestamp, message JSON, padding,
rekey payloads). Wire fields ``options`` and ``server_seq`` kept for backwards
compatibility (will be removed later).
- Avoid hard dependency lock-in: provider abstraction first, library choice second.
- Avoid introducing Ultra-mode complexity before Normal mode exits Phase 3 criteria.
- All CCCData fields are user-editable at channel creation (defaults pre-selected).
- ``groupMasterSeed`` (512-bit) is generated per channel and never sent in plaintext.
Pros / Cons
============
Pros
----
- Strong hedge against single-primitive failure.
- Per-group / per-message mutation increases attacker cost dramatically.
- FS/PCS path preserved by ratchet-first implementation.
- Ephemeral relay architecture aligns with larger ciphertext overhead.
- User cryptographic sovereignty no server/developer override.
- Modular provider system allows mixing libraries for maximum implementation diversity.
Cons / Risks
------------
- CPU/battery overhead increases significantly in Ultra mode.
- Multi-library/FFI surface increases audit complexity.
- Logic bugs in ratchet/cascade orchestration are a principal risk.
- Diminishing returns beyond roughly 2024 cascade layers.
- CCCData complexity requires careful UI design to avoid user confusion.
Conclusion
==========
Phase 0 is complete: the provider abstraction boundary, isolate-backed crypto
pipeline, channel-scoped profiles, and baseline security tests are all in place.
Phase 1 is near completion: deterministic key schedule, AEAD associated-data
binding, and unified message + attachment crypto context are integrated and
tested. End-to-end relay-flow verification is the remaining gate.
The path forward:
1. Close Phase 1 with relay-flow E2E verification.
2. Phase 2: Double Ratchet for real forward secrecy (``NormalRatchet`` class).
3. Phase 3: Full Normal-mode target parity (hybrid KEM, Ascon, CCCData UI — pure-Dart providers).
4. Phase 4: wolfSSL FFI for native primitives + multi-provider diversity.
5. Phase 5: Ultra Secret mode with triple-compound ratchet.
6. Phase 6: Encrypted channel blob for secure invite sharing.
7. Phase 7: Audit, fuzzing, and deployment hardening.

View File

@ -0,0 +1,246 @@
================================================================================
Ultra Mode Encryption Specification Hydra Apocalypse Ratchet
================================================================================
**Project**: World's Most Secure Messaging App
**Mode**: Ultra Secret Mode (opt-in per channel)
**Codename**: Hydra Apocalypse Ratchet Ultra Mode
**Author**: JohnΞ (@j3g)
**Date**: February 18, 2026
**Version**: 1.0 Complete, implementation-ready specification for Ultra Mode (Dart + Flutter + Hive + gRPC)
**Primary Design Goal (Non-Negotiable)**
=======================================
This is the **world's most secure messaging app**.
Ultra Mode exists for users who assume nation-states have already backdoored or broken major primitives (AES, Kyber, McEliece, etc.).
It forces an attacker to break **many independent cryptographic lineages simultaneously** while guessing a **secret per-channel, per-message cascade sequence** that is never sent over the wire.
Ultra Mode is **always stronger** than Normal Mode and **stronger than any known protocol** (Signal, PQXDH, etc.) in the threat model of "one primitive family is secretly rotten".
Executive Summary
=================
Ultra Mode = **Triple-Compound Ratchet**:
1. Inner configurable PQ Hybrid Double Ratchet (user-defined via CCCData)
2. Massive secret cascade of 1624 layers (from user-chosen providers + algorithms)
3. Per-message cascade sequence mutation (deterministic, never sent on wire)
Everything is defined at **channel creation time** in CCCData.
No ratchet_public, no metadata leaks, no server influence.
gRPC is used for device ↔ relay transport (but relay remains stateless/ephemeral RAM forwarder).
CCCData Ultra Mode Extension (Dart + Hive)
============================================
.. code-block:: dart
@HiveType(typeId: 42)
class CCCData extends HiveObject {
@HiveField(0) final int version = 1;
@HiveField(1) final String mode = 'ultra'; // 'normal' or 'ultra'
// Providers (user selects which libraries are allowed)
@HiveField(2) final List<String> enabledProviders; // ['wolfssl', 'boringssl', 'libsodium', 'openssl']
// Modular category configs (user picks ordered lists per category)
@HiveField(3) final Map<String, List<CryptoSelection>> kemConfigs;
@HiveField(4) final Map<String, List<CryptoSelection>> aeadConfigs; // inner + outer
@HiveField(5) final Map<String, List<CryptoSelection>> kdfConfigs;
@HiveField(6) final Map<String, List<CryptoSelection>> hashConfigs;
// ULTRA-SPECIFIC SETTINGS (user configurable)
@HiveField(20) final int cascadeLength; // 1624 (default 20)
@HiveField(21) final int cascadePoolSize; // 4864 (default 56)
@HiveField(22) final String cascadeOrdering; // 'strongest_first' (default), 'random', 'user_defined'
@HiveField(23) final bool cascadeReKeyAfter; // re-key inner root after cascade (default true)
@HiveField(24) final int sequenceRatchetStrength; // bits of KMAC output used for permutation seed (default 256)
// Shared ratchet settings (same as Normal Mode)
@HiveField(7) final int ratchetFrequency;
@HiveField(8) final bool symmetricRatchetEveryMessage;
@HiveField(9) final int rekeyInterval;
@HiveField(10) final String rekeyMode;
// Padding & anti-distinguisher
@HiveField(11) final int minPaddingBytes; // default 64
@HiveField(12) final int blockSize; // default 64
// Channel identifiers
@HiveField(13) final Uint8List groupId; // 32 bytes
@HiveField(14) final int createdAt;
@HiveField(15) final Uint8List creatorDeviceId;
CCCData({ /* all required fields */ });
}
@HiveType(typeId: 43)
class CryptoSelection extends HiveObject {
@HiveField(0) String provider;
@HiveField(1) String algorithm; // 'AES256GCM', 'Kyber1024', 'Serpent256CTR', 'ClassicMcEliece460896', etc.
@HiveField(2) int priority; // round-robin order
}
**How the Cascade Pool is Built** (at channel load time):
- For every enabledProvider, query the provider's registry for algorithms in these categories: symmetric, aead, pq_kem, pq_signature, hash_based.
- Build a flat list of 4864 unique (provider, algorithm) pairs.
- User can override the pool via advanced UI (future).
Channel Creation & Sharing
==========================
1. User selects "Ultra Secret" → opens advanced CCCData editor (all fields editable).
2. App generates groupId, groupMasterSeed (512-bit), initial ratchet states.
3. CCCData + groupMasterSeed stored in Hive (encrypted with device master key).
4. Invite: encrypt entire CCCData + groupMasterSeed under recipient's public key → send as single blob.
5. Recipient imports → verifies groupId matches → stores in Hive → both have identical crypto config forever.
gRPC Wire Protocol (RelayMessage)
=================================
.. code-block:: proto
service RelayService {
rpc SendMessage(RelayMessage) returns (Empty);
rpc SubscribeToChannel(ChanSubscribe) returns (stream RelayMessage);
}
message RelayMessage {
uint32 from_rly_user_id = 1;
uint32 to_rly_chan_id = 2;
uint32 options = 3; // backwards compat (will be removed)
bytes data = 4; // FULL E2EE ciphertext blob
uint64 server_seq = 5; // backwards compat (will be removed)
}
**data** field contains (serialized, then encrypted):
- timestamp (for UI)
- message JSON / content
- padding
- rekey payload (if any)
- Everything else
Ultra Mode Triple-Compound Ratchet (Dart Implementation)
========================================================
```dart
class UltraRatchet {
final CCCData ccc;
final Uint8List groupMasterSeed;
// Per-sender chains (inner Double Ratchet)
Map<int, ChainState> sendingChains = {};
Map<int, ChainState> receivingChains = {};
// Current sequence seed (mutates per message)
Uint8List currentSequenceSeed = Uint8List(32);
Uint8List getBaseMessageKey(int fromRlyUserId, int senderCounter, bool isSending) {
// 1. Advance inner symmetric ratchet
final chain = isSending ? sendingChains[fromRlyUserId]! : receivingChains[fromRlyUserId]!;
Uint8List baseKey = KDF(ccc.kdfConfigs, chain.currentKey, senderCounter.toBytes());
// 2. Derive seq_seed for this message
final seqSeed = KMAC256(baseKey, utf8.encode('seq-seed') + groupId + senderCounter.toBytes(8));
// 3. Generate secret cascade permutation (16-24 layers)
final cascadeSequence = deterministicPermutation(seqSeed, ccc.cascadePoolSize, ccc.cascadeLength);
// 4. (Optional) re-key inner root after cascade later in flow
return baseKey;
}
List<int> deterministicPermutation(Uint8List seed, int poolSize, int length) {
final rng = ChaCha20(seed); // seeded CSPRNG
var indices = List.generate(poolSize, (i) => i);
// Fisher-Yates shuffle using rng
for (int i = poolSize - 1; i > 0; i--) {
final j = rng.nextInt(i + 1);
final temp = indices[i];
indices[i] = indices[j];
indices[j] = temp;
}
return indices.sublist(0, length); // return ordered indices into pool
}
Uint8List cascadeEncrypt(Uint8List plaintext, List<int> sequence, Uint8List baseKey, int senderCounter) {
Uint8List ct = plaintext;
for (int i = 0; i < sequence.length; i++) {
final selection = cascadePool[sequence[i]]; // global pool built from CCCData
final layerInfo = utf8.encode('layer-$i') + groupId + senderCounter.toBytes(8);
final layerKey = HKDF(ccc.kdfConfigs, baseKey, layerInfo, selection.keyLength);
ct = encryptOneLayer(selection.provider, selection.algorithm, layerKey, ct);
}
return ct;
}
// Decrypt is reverse order
}
```
Message Sending Flow (Ultra Mode) Step-by-Step
================================================
1. Load CCCData + build cascadePool from enabledProviders.
2. Advance inner ratchet → base_message_key + seqSeed.
3. Generate secret cascadeSequence (1624 layers).
4. Pad plaintext: padToMultipleOf(ccc.blockSize) + random(ccc.minPaddingBytes) from KDF(baseKey).
5. Inner AEAD: round-robin using ccc.aeadConfigs on padded plaintext → innerCt.
6. Cascade encrypt innerCt → cascadeCt.
7. If ccc.cascadeReKeyAfter: newInnerRoot = KMAC256(cascadeCt + oldRoot).
8. Outer AEAD (fast fixed from aeadConfigs) on cascadeCt → finalData.
9. Build RelayMessage → send via gRPC.
Message Receiving Flow
======================
1. Receive RelayMessage via gRPC stream.
2. Immediately persist raw RelayMessage to Hive 'messages' box (key = groupId + fromRlyUserId + senderCounter).
3. Load CCCData → advance receiving inner ratchet → derive baseKey + seqSeed + cascadeSequence.
4. Decrypt finalData (outer AEAD) → cascadeCt.
5. Cascade decrypt (reverse order) → innerCt.
6. Inner AEAD decrypt → plaintext + timestamp.
7. If RekeyPayload present → apply new root secrets.
Local Hive Storage (Ultra Mode)
===============================
Same as Normal Mode + extra fields for debugging:
- rawRelayMessage (full protobuf bytes)
- encryptedBaseMessageKey (AES-GCM under device master key)
- cascadeSequenceCache (optional, for fast reload encrypted)
- plaintextCache (optional)
Rekey Flow (PCS)
================
- Triggered by user button or ccc.rekeyInterval.
- Generate fresh hybrid KEM secret (using ccc.kemConfigs).
- Create RekeyPayload inside the encrypted data field.
- Recipient applies after decrypt → future messages use new root.
Security Analysis Why Ultra Mode Is Unbeatable
=================================================
- Attacker must break 1624 diverse primitives (different providers, different math) in unknown secret order that changes every message.
- Combinatorial explosion per message per channel.
- No public material on wire → zero metadata beyond absolute minimum.
- User controls every provider and algorithm.
- Survives secret break of any single family (AES, Kyber, McEliece, etc.).
Implementation Checklist (Dart/Flutter + gRPC + Hive)
=====================================================
1. Extend CryptoProvider registry with all libraries (wolfssl_flutter, boringssl, libsodium_flutter, etc.).
2. Hive adapters for CCCData, CryptoSelection, StoredMessage.
3. UltraRatchet class with cascadePool builder + deterministicPermutation.
4. gRPC client/service with RelayMessage.
5. Channel creation screen with Ultra toggle + advanced tabs for cascade settings.
6. Cascade encrypt/decrypt dispatcher (test with 4 layers first, then full 20).
7. Rekey UI button + auto logic.
8. Full end-to-end tests with different CCCData combinations.
9. Performance benchmarking on real devices (Ultra mode is heavy show battery warning).

View File

@ -0,0 +1,162 @@
================================================================================
Ultra Mode Complete Detailed Design & Persistence Specification
================================================================================
**Project**: World's Most Secure Messaging App
**Mode**: Ultra Secret Mode (opt-in per channel)
**Date**: February 20, 2026
**Version**: 1.0 Written for someone who has never built a ratchet before
This document explains **everything** from first principles, just like the Normal Mode document.
Every term is defined the first time it appears.
Every high-level statement is followed by the low-level cryptographic details.
No assumptions.
No gaps.
1. What Makes Ultra Mode Different from Normal Mode
===================================================
Ultra Mode is for people who believe a nation-state or powerful adversary has already secretly broken one major cryptographic family (e.g., AES, Kyber, McEliece, etc.).
The core idea:
- Instead of trusting one or two algorithms (like Normal Mode does),
- Ultra Mode puts **1624 different algorithms in a secret sequence**.
- The sequence is different for every message and never sent over the wire.
- The attacker must break **all** of them in the unknown order to read a message.
This is called a **cascade** (layers of encryption).
We combine it with a ratchet so we still get forward secrecy and post-compromise security.
2. The Triple-Compound Ratchet (High-Level + Low-Level)
=======================================================
Ultra Mode uses **three ratchet layers** working together:
**Layer 1 Inner Ratchet**
Same as Normal Mode Double Ratchet (symmetric chain + occasional asymmetric reset).
This is the fast daily engine.
**Low-Level (same as Normal Mode but inside the cascade)**:
- Starts with root secret from channel creation.
- Symmetric advance: chainKey = KDF(chainKey, "next" + groupId)
- Base message key = KDF(chainKey, counter + groupId)
- Asymmetric reset (every N messages): fresh KEM shared secret mixed in.
**Layer 2 Secret Cascade**
After the inner ratchet encrypts, we encrypt **again** with 1624 layers of different algorithms in a secret order.
**Low-Level**:
- From the inner base_message_key, derive a 32-byte seq_seed:
seq_seed = KMAC256(base_message_key, "seq-seed" + groupId + counter)
- Generate secret sequence:
Use ChaCha20 (seeded by seq_seed) as CSPRNG → Fisher-Yates shuffle of pool indices → take first 1624.
- For each layer in sequence:
layerKey = HKDF(base_message_key, "layer-" + i + groupId + counter, keyLength)
ciphertext = encrypt(ciphertext, layerKey, algorithm from pool)
- Strongest algorithms first (McEliece/Kyber early, AES last).
**Layer 3 Sequence Mutation**
The seq_seed itself changes every message (ratcheted forward), so the cascade sequence changes every message.
**Low-Level**:
- After each message, update seq_seed base:
new_seq_seed_base = KMAC256(seq_seed, "mutate" + groupId + counter)
- Next message uses this as starting point for new seq_seed.
3. What Exactly Is Stored on Your Device (Very Specific)
========================================================
For each chat and each sender:
- **CCCData** full user settings (providers, cascadeLength=20, poolSize=56, etc.)
- **Raw encrypted wire blobs** every message's exact RelayMessage bytes (this is the permanent chat history)
- **Ratchet state** (one per sender per channel):
- encryptedRootKey (AES-256-GCM under device master key)
- currentChainKey (inner ratchet, 32 bytes)
- lastProcessedCounter (highest decrypted message)
- checkpoints (chain key snapshots every 500 messages)
No per-message keys.
No full historical chain.
Only current state + raw blobs.
4. Message Flow From One User to Another
========================================
**Sending Side**
1. Type message.
2. Load CCCData (builds cascade pool from your providers).
3. Load sending ratchet state.
4. Advance inner symmetric chain (KDF "next").
5. Derive base_message_key (KDF chain + counter).
6. Derive seq_seed (KMAC256 base + "seq-seed" + counter).
7. Generate secret cascade sequence (ChaCha20 shuffle).
8. Pad plaintext.
9. Inner AEAD round-robin (your chosen list).
10. Cascade encrypt (1624 layers, strongest first).
11. Optional re-key inner root from cascade output.
12. Outer AEAD (fast fixed).
13. Build RelayMessage → send via gRPC.
14. Update & save ratchet state.
**Receiving Side**
1. Receive RelayMessage → store raw bytes immediately.
2. Load CCCData + receiving state.
3. Advance inner ratchet to match counter.
4. Derive base_message_key.
5. Derive seq_seed + cascade sequence (same as sender).
6. Decrypt outer AEAD.
7. Cascade decrypt (reverse order).
8. Inner AEAD decrypt.
9. Remove padding → show message.
10. Update & save state.
5. How We Load Messages on Cold Start
=====================================
**Recent Messages (instant)**
1. Authenticate → device master key.
2. Load CCCData + ratchet state.
3. Load newest raw blobs.
4. For messages > lastProcessedCounter:
- Advance inner ratchet forward.
- Derive base + seq_seed + cascade.
- Full decrypt.
- Update state.
**Older Messages (paranoid, lazy)**
1. Scroll up → old messages show "Locked".
2. Tap "Unlock older" → re-authenticate (passphrase/biometrics).
3. App replays inner ratchet forward from last state to old counter.
4. Derives base + seq_seed + cascade for each.
5. Decrypts raw blobs.
6. Caches plaintext (encrypted under device master key).
7. Updates state and checkpoints.
6. Starting the Ratchet Chain (Channel Creation / Invite)
==========================================================
1. Creator generates 512-bit root secret.
2. Encrypts under recipients public key (KEM from CCCData).
3. Sends in invite.
4. Recipient decrypts root secret.
5. Both initialize:
inner chain keys = KDF(root, "inner-sending/receiving" + groupId)
6. Save encrypted root + initial chain keys.

View File

@ -0,0 +1,17 @@
/// Attachment crypto context for BG attachment read/write operations.
///
/// This data container carries the resolved channel cipher sequence and the
/// key-schedule-enriched cipher parameters so attachment crypto calls can use
/// one consistent context.
class AttachmentCryptoContext {
/// Ordered cipher chain applied by CCC for this attachment operation.
final List<int> cipherSequence;
/// Effective cipher parameters, including key-schedule metadata.
final Map<String, dynamic> cipherParams;
const AttachmentCryptoContext({
required this.cipherSequence,
required this.cipherParams,
});
}

View File

@ -0,0 +1,321 @@
///
/// Channel-level CCC profile derivation.
///
import 'package:letusmsg/ccc/ccc_kdf.dart';
import 'package:letusmsg/ccc/cipher_constants.dart';
import 'package:letusmsg/ccc/ccc_provider_spec.dart';
import 'package:letusmsg/data/ccc_data.dart';
/// One resolved cipher execution step for a channel CCC route.
///
/// The step identifies a primary provider and optional ordered fallbacks.
class CccRouteStep {
final int cipher;
final CccCryptoProvider provider;
final List<CccCryptoProvider> fallbackProviders;
const CccRouteStep({
required this.cipher,
required this.provider,
required this.fallbackProviders,
});
Map<String, dynamic> toMap() {
return {
'cipher': cipher,
'provider': provider.name,
'fallbackProviders': fallbackProviders.map((item) => item.name).toList(growable: false),
};
}
}
/// Concrete cipher configuration derived from per-channel `CCCData`.
class ChannelCccProfile {
final int combo;
final int iterations;
final CccExecutionMode executionMode;
final bool allowFallback;
final List<CccProviderSpec> providers;
final List<CccRouteStep> route;
final List<int> cipherSequence;
final Map<String, dynamic> cipherParams;
const ChannelCccProfile({
required this.combo,
required this.iterations,
required this.executionMode,
required this.allowFallback,
required this.providers,
required this.route,
required this.cipherSequence,
required this.cipherParams,
});
static const int _maxIterations = 16;
factory ChannelCccProfile.fromCccData(CCCData? cccData) {
final combo = cccData?.combo ?? 0;
final rawIterations = cccData?.iterrations ?? 0;
final kdfFunctionValue = normalizeCccKdfFunctionValue(cccData?.kdfFunction);
final kdfFunction = cccKdfFunctionFromInt(kdfFunctionValue);
final executionMode = cccExecutionModeFromString(cccData?.executionMode ?? 'auto');
final iterations = rawIterations <= 0
? 1
: rawIterations.clamp(1, _maxIterations);
final allowFallback = executionMode != CccExecutionMode.strict;
final providers = _providersForCombo(combo);
final route = _resolveRoute(providers, executionMode: executionMode, allowFallback: allowFallback);
final baseSequence = route.map((item) => item.cipher).toList(growable: false);
final expandedSequence = <int>[];
for (var i = 0; i < iterations; i++) {
expandedSequence.addAll(baseSequence);
}
final params = <String, dynamic>{
...CipherConstants.DEFAULT_CIPHER_PARAMS,
'ccc_combo': combo,
'ccc_iterations': iterations,
'ccc_kdf_function': kdfFunction.name,
'ccc_kdf_function_value': kdfFunction.value,
'ccc_execution_mode': executionMode.name,
'ccc_allow_fallback': allowFallback,
'ccc_providers': providers.map((provider) => provider.toMap()).toList(growable: false),
'ccc_route': route.map((step) => step.toMap()).toList(growable: false),
// Phase 3: length-hiding padding parameters
'ccc_min_padding_bytes': cccData?.minPaddingBytes ?? 32,
'ccc_block_size': cccData?.blockSize ?? 256,
};
return ChannelCccProfile(
combo: combo,
iterations: iterations,
executionMode: executionMode,
allowFallback: allowFallback,
providers: providers,
route: route,
cipherSequence: expandedSequence,
cipherParams: params,
);
}
static List<CccRouteStep> _resolveRoute(
List<CccProviderSpec> providers, {
required CccExecutionMode executionMode,
required bool allowFallback,
}) {
final strictRoute = <CccRouteStep>[];
for (final provider in providers) {
for (final cipher in provider.ciphers) {
strictRoute.add(CccRouteStep(
cipher: cipher,
provider: provider.provider,
fallbackProviders: const [],
));
}
}
if (executionMode == CccExecutionMode.strict) {
return strictRoute;
}
final optimizedRoute = <CccRouteStep>[];
for (final step in strictRoute) {
final ordered = _orderedProviderCandidates(
cipher: step.cipher,
configuredProviders: providers.map((item) => item.provider).toSet(),
executionMode: executionMode,
);
final primary = ordered.isNotEmpty ? ordered.first : step.provider;
final fallbacks = allowFallback
? ordered.where((provider) => provider != primary).toList(growable: false)
: const <CccCryptoProvider>[];
optimizedRoute.add(CccRouteStep(
cipher: step.cipher,
provider: primary,
fallbackProviders: fallbacks,
));
}
return optimizedRoute;
}
static List<CccCryptoProvider> _orderedProviderCandidates({
required int cipher,
required Set<CccCryptoProvider> configuredProviders,
required CccExecutionMode executionMode,
}) {
final configured = configuredProviders.where((provider) {
return CccProviderCatalog.supports(provider, cipher);
}).toList(growable: false);
if (executionMode == CccExecutionMode.efficient) {
final rankedConfigured = List<CccCryptoProvider>.from(configured)
..sort((a, b) {
final capA = CccProviderCatalog.capability(a, cipher);
final capB = CccProviderCatalog.capability(b, cipher);
final perf = (capB?.efficiencyScore ?? 0).compareTo(capA?.efficiencyScore ?? 0);
if (perf != 0) return perf;
return (capB?.reliabilityScore ?? 0).compareTo(capA?.reliabilityScore ?? 0);
});
return rankedConfigured;
}
final availableRanked = CccProviderCatalog.providersSupporting(cipher, availableOnly: true)
.where(configuredProviders.contains)
.toList(growable: false);
final unavailableRanked = CccProviderCatalog.providersSupporting(cipher, availableOnly: false)
.where((provider) => configuredProviders.contains(provider) && !availableRanked.contains(provider))
.toList(growable: false);
return [
...availableRanked,
...unavailableRanked,
];
}
static List<CccProviderSpec> _providersForCombo(int combo) {
switch (combo) {
case 0:
// Combo 0 is reserved as the plaintext / unencrypted legacy combo.
// It produces an empty cipher sequence so the CCC pipeline serializes
// and deserializes JSON only, with no encryption layers applied.
// Backwards-compatible with all channels created before Normal Mode.
// When Normal Mode is complete, CCCData.blank() will be updated to a
// real encrypted combo (e.g. combo 5 or higher) and combo 0 will
// remain available only for migration/legacy channels.
return [];
case 1:
return [
CccProviderSpec(
provider: CccCryptoProvider.wolfssl,
ciphers: [
CipherConstants.AES_GCM_256,
CipherConstants.CHACHA20_POLY1305,
],
),
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: [
CipherConstants.HMAC_SHA512,
CipherConstants.BLAKE2B,
],
),
];
case 2:
return [
CccProviderSpec(
provider: CccCryptoProvider.boringssl,
ciphers: [
CipherConstants.CHACHA20_POLY1305,
CipherConstants.XCHACHA20_POLY1305,
],
),
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: [
CipherConstants.AES_GCM_256,
],
),
];
case 3:
return [
CccProviderSpec(
provider: CccCryptoProvider.openssl,
ciphers: [
CipherConstants.AES_GCM_256,
],
),
CccProviderSpec(
provider: CccCryptoProvider.wolfssl,
ciphers: [
CipherConstants.CHACHA20_POLY1305,
],
),
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: [
CipherConstants.BLAKE2B,
],
),
];
case 4:
return [
CccProviderSpec(
provider: CccCryptoProvider.wolfssl,
ciphers: [
CipherConstants.XCHACHA20_POLY1305,
],
),
CccProviderSpec(
provider: CccCryptoProvider.openssl,
ciphers: [
CipherConstants.HMAC_SHA512,
],
),
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: [
CipherConstants.BLAKE2B,
],
),
];
// --- Basic / user-selectable combos (5-9) ---
case 5:
// Basic: single AES-256-GCM
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.BASIC_AES_SEQUENCE),
),
];
case 6:
// Basic: single ChaCha20-Poly1305
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.BASIC_CHACHA_SEQUENCE),
),
];
case 7:
// Basic: single XChaCha20-Poly1305
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.BASIC_XCHACHA_SEQUENCE),
),
];
case 8:
// Dual AEAD: AES-256-GCM + ChaCha20-Poly1305
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.DUAL_AEAD_SEQUENCE),
),
];
case 9:
// Triple AEAD: AES + ChaCha20 + XChaCha20
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.TRIPLE_AEAD_SEQUENCE),
),
];
default:
// Unknown combo falls back to standard 5-layer
return [
CccProviderSpec(
provider: CccCryptoProvider.cryptography,
ciphers: List<int>.from(CipherConstants.PHASE1_SEQUENCE),
),
];
}
}
}

View File

@ -0,0 +1,632 @@
///
/// Cipher implementations for isolate workers
/// Uses cryptography package for all crypto operations
///
import 'dart:convert';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
import 'ccc_iso_operation.dart';
import 'ccc_iso_result.dart';
import 'cipher_constants.dart';
/// Main crypto worker function that runs in isolates
@pragma('vm:entry-point')
Future<Map<String, dynamic>> cryptoWorkerFunction(Map<String, dynamic> data) async {
final startTime = DateTime.now();
// Use isolate hash code as a stable worker identifier
final isolateHash = Isolate.current.hashCode.abs() % 10000;
final workerId = 'isolate_$isolateHash';
try {
// Deserialize operation
final operation = CryptoOperation.fromMap(data);
// Execute cipher chain
final resultData = await _executeCipherChain(
operation.data,
operation.cipherSequence,
operation.params,
operation.type,
);
final processingTime = DateTime.now().difference(startTime);
// Create success result
final result = CryptoResult.success(
operationId: operation.id,
data: resultData,
processingTime: processingTime,
workerId: workerId,
);
return result.toMap();
} catch (error, stackTrace) {
final processingTime = DateTime.now().difference(startTime);
// Create error result
final result = CryptoResult.error(
operationId: data['id'] as String? ?? 'unknown',
error: error.toString(),
stackTrace: stackTrace.toString(),
processingTime: processingTime,
workerId: workerId,
);
return result.toMap();
}
}
/// Execute cipher chain based on operation type
Future<List<int>> _executeCipherChain(List<int> inputData, List<int> cipherSequence,
Map<String, dynamic> params, OperationType operationType) async {
List<int> data = inputData;
final associatedData = _buildAssociatedDataBytes(params);
if (operationType == OperationType.encrypt) {
// Apply length-hiding padding before encryption
data = _applyPadding(data, params);
// Forward execution for encryption
for (var i = 0; i < cipherSequence.length; i++) {
data = await _executeCipher(
data,
cipherSequence[i],
params,
true,
associatedData: associatedData,
layerIndex: i,
);
}
} else {
// Reverse execution for decryption peel outermost layer first
for (var i = cipherSequence.length - 1; i >= 0; i--) {
data = await _executeCipher(
data,
cipherSequence[i],
params,
false,
associatedData: associatedData,
layerIndex: i,
);
}
// Strip length-hiding padding after decryption
data = _stripPadding(data);
}
return data;
}
/// Execute single cipher operation.
///
/// [layerIndex] identifies this step's position in the cipher sequence.
/// Combined with [cipherConstant], it allows deterministic per-layer key
/// derivation when `phase1_root_key_b64` is present in [params].
Future<List<int>> _executeCipher(List<int> data, int cipherConstant,
Map<String, dynamic> params, bool isEncrypt,
{List<int>? associatedData, int layerIndex = 0}) async {
switch (cipherConstant) {
// Key Derivation Functions
case CipherConstants.ARGON2ID:
return await _executeArgon2id(data, params);
// AEAD Ciphers
case CipherConstants.AES_GCM_256:
return await _executeAesGcm256(data, params, isEncrypt,
associatedData: associatedData, layerIndex: layerIndex);
case CipherConstants.CHACHA20_POLY1305:
return await _executeChacha20Poly1305(data, params, isEncrypt,
associatedData: associatedData, layerIndex: layerIndex);
case CipherConstants.XCHACHA20_POLY1305:
return await _executeXChacha20Poly1305(data, params, isEncrypt,
associatedData: associatedData, layerIndex: layerIndex);
// MAC Algorithms
case CipherConstants.HMAC_SHA512:
return await _executeHmacSha512(data, params, isEncrypt, layerIndex: layerIndex);
case CipherConstants.BLAKE2B:
return await _executeBlake2b(data, params, isEncrypt);
default:
throw UnsupportedError('Cipher not implemented: ${CipherConstants.getCipherName(cipherConstant)}');
}
}
/// Argon2id key derivation (always applied, regardless of encrypt/decrypt)
Future<List<int>> _executeArgon2id(List<int> data, Map<String, dynamic> params) async {
final algorithm = Argon2id(
memory: params['argon2_memory'] as int? ?? 64 * 1024,
parallelism: params['argon2_parallelism'] as int? ?? 4,
iterations: params['argon2_iterations'] as int? ?? 3,
hashLength: params['argon2_hash_length'] as int? ?? 32,
);
// Use first 16 bytes as salt, or generate if data is too short
Uint8List salt;
if (data.length >= 16) {
salt = Uint8List.fromList(data.take(16).toList());
} else {
salt = Uint8List.fromList(List.generate(16, (i) => i));
}
final secretKey = await algorithm.deriveKeyFromPassword(
password: String.fromCharCodes(data),
nonce: salt.toList(),
);
final keyBytes = await secretKey.extractBytes();
return keyBytes;
}
/// AES-256-GCM encryption/decryption.
///
/// When `phase1_root_key_b64` is present in [params], derives a deterministic
/// per-layer key from the channel root key (real E2E encryption).
/// Output format: `[12B nonce][ciphertext][16B MAC]`.
///
/// Legacy mode (no root key): generates a random key per encryption and
/// embeds it in the output: `[32B key][12B nonce][ciphertext][16B MAC]`.
Future<List<int>> _executeAesGcm256(
List<int> data,
Map<String, dynamic> params,
bool isEncrypt, {
List<int>? associatedData,
int layerIndex = 0,
}) async {
final algorithm = AesGcm.with256bits();
final useDerivedKey = params.containsKey('phase1_root_key_b64');
if (isEncrypt) {
final SecretKey secretKey;
if (useDerivedKey) {
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.AES_GCM_256, 32);
secretKey = SecretKeyData(keyBytes);
} else {
secretKey = await algorithm.newSecretKey();
}
final secretBox = await algorithm.encrypt(
data,
secretKey: secretKey,
aad: associatedData ?? const <int>[],
);
if (useDerivedKey) {
// Derived-key format: [nonce][ciphertext][mac]
return [...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
}
// Legacy format: [key][nonce][ciphertext][mac]
final keyBytes = await secretKey.extractBytes();
return [...keyBytes, ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
} else {
if (useDerivedKey) {
// Derived-key format: [12B nonce][ciphertext][16B mac]
if (data.length < 12 + 16) {
throw ArgumentError('Invalid AES-GCM derived-key data length');
}
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.AES_GCM_256, 32);
final nonce = data.sublist(0, 12);
final cipherText = data.sublist(12, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
// Legacy format: [32B key][12B nonce][ciphertext][16B mac]
if (data.length < 32 + 12 + 16) {
throw ArgumentError('Invalid AES-GCM data length');
}
final keyBytes = data.sublist(0, 32);
final nonce = data.sublist(32, 44);
final cipherText = data.sublist(44, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
}
/// ChaCha20-Poly1305 encryption/decryption.
///
/// Derived-key output: `[12B nonce][ciphertext][16B MAC]`.
/// Legacy output: `[32B key][12B nonce][ciphertext][16B MAC]`.
Future<List<int>> _executeChacha20Poly1305(
List<int> data,
Map<String, dynamic> params,
bool isEncrypt, {
List<int>? associatedData,
int layerIndex = 0,
}) async {
final algorithm = Chacha20.poly1305Aead();
final useDerivedKey = params.containsKey('phase1_root_key_b64');
if (isEncrypt) {
final SecretKey secretKey;
if (useDerivedKey) {
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.CHACHA20_POLY1305, 32);
secretKey = SecretKeyData(keyBytes);
} else {
secretKey = await algorithm.newSecretKey();
}
final secretBox = await algorithm.encrypt(
data,
secretKey: secretKey,
aad: associatedData ?? const <int>[],
);
if (useDerivedKey) {
return [...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
}
final keyBytes = await secretKey.extractBytes();
return [...keyBytes, ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
} else {
if (useDerivedKey) {
if (data.length < 12 + 16) {
throw ArgumentError('Invalid ChaCha20-Poly1305 derived-key data length');
}
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.CHACHA20_POLY1305, 32);
final nonce = data.sublist(0, 12);
final cipherText = data.sublist(12, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
if (data.length < 32 + 12 + 16) {
throw ArgumentError('Invalid ChaCha20-Poly1305 data length');
}
final keyBytes = data.sublist(0, 32);
final nonce = data.sublist(32, 44);
final cipherText = data.sublist(44, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
}
/// XChaCha20-Poly1305 encryption/decryption.
///
/// Derived-key output: `[24B nonce][ciphertext][16B MAC]`.
/// Legacy output: `[32B key][24B nonce][ciphertext][16B MAC]`.
Future<List<int>> _executeXChacha20Poly1305(
List<int> data,
Map<String, dynamic> params,
bool isEncrypt, {
List<int>? associatedData,
int layerIndex = 0,
}) async {
final algorithm = Xchacha20.poly1305Aead();
final useDerivedKey = params.containsKey('phase1_root_key_b64');
if (isEncrypt) {
final SecretKey secretKey;
if (useDerivedKey) {
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.XCHACHA20_POLY1305, 32);
secretKey = SecretKeyData(keyBytes);
} else {
secretKey = await algorithm.newSecretKey();
}
final secretBox = await algorithm.encrypt(
data,
secretKey: secretKey,
aad: associatedData ?? const <int>[],
);
if (useDerivedKey) {
return [...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
}
final keyBytes = await secretKey.extractBytes();
return [...keyBytes, ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes];
} else {
if (useDerivedKey) {
if (data.length < 24 + 16) {
throw ArgumentError('Invalid XChaCha20-Poly1305 derived-key data length');
}
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.XCHACHA20_POLY1305, 32);
final nonce = data.sublist(0, 24);
final cipherText = data.sublist(24, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
if (data.length < 32 + 24 + 16) {
throw ArgumentError('Invalid XChaCha20-Poly1305 data length');
}
final keyBytes = data.sublist(0, 32);
final nonce = data.sublist(32, 56);
final cipherText = data.sublist(56, data.length - 16);
final macBytes = data.sublist(data.length - 16);
return await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: SecretKeyData(keyBytes),
aad: associatedData ?? const <int>[],
);
}
}
/// Build canonical associated-data bytes from Phase 1 params.
///
/// The worker expects `phase1_associated_data` in params and canonicalizes it
/// to stable UTF-8 JSON so encrypt/decrypt use identical AEAD AAD bytes.
///
/// Phase 1 exclusions (both removed so sender/receiver produce identical AD):
/// - `direction`: sender uses 'out', receiver uses 'in' for the same ciphertext.
/// - `message_sequence`: sender encrypts before the relay assigns server_seq,
/// so the receiver's relay-assigned sequence would mismatch. Sequence binding
/// will be re-enabled in Phase 2 when the ratchet provides agreed-upon counters.
List<int>? _buildAssociatedDataBytes(Map<String, dynamic> params) {
final phase1 = params['phase1_associated_data'];
if (phase1 == null) return null;
final canonical = _canonicalizeJsonSafe(phase1);
if (canonical == null) return null;
if (canonical is Map<String, dynamic>) {
final normalized = Map<String, dynamic>.from(canonical)
..remove('direction')
..remove('message_sequence');
return utf8.encode(jsonEncode(normalized));
}
if (canonical is List) {
return utf8.encode(jsonEncode(canonical));
}
return utf8.encode(canonical.toString());
}
dynamic _canonicalizeJsonSafe(dynamic value) {
if (value is Map) {
final keys = value.keys.map((item) => item.toString()).toList()..sort();
final normalized = <String, dynamic>{};
for (final key in keys) {
normalized[key] = _canonicalizeJsonSafe(value[key]);
}
return normalized;
}
if (value is List) {
return value.map(_canonicalizeJsonSafe).toList(growable: false);
}
return value;
}
// ---------------------------------------------------------------------------
// Derived-key helpers
// ---------------------------------------------------------------------------
/// Derive a deterministic per-layer encryption key from the channel root key.
///
/// Uses SHA-512 (always, regardless of channel KDF selection) to ensure
/// sufficient output length for all cipher types:
/// - AEAD ciphers (AES-256-GCM, ChaCha20, XChaCha20): 32 bytes
/// - HMAC-SHA512: 64 bytes
///
/// The derivation is deterministic: identical inputs on sender and receiver
/// produce the same key, enabling real E2E encryption without embedding keys
/// in the ciphertext.
///
/// Formula: `SHA-512(rootKeyBytes || "|lk|{layerIndex}|c|{cipherConstant}")`
/// truncated to [keyLength] bytes.
Future<List<int>> _deriveLayerKey(
Map<String, dynamic> params,
int layerIndex,
int cipherConstant,
int keyLength,
) async {
final rootKeyB64 = params['phase1_root_key_b64'] as String;
final rootKeyBytes = _decodeBase64UrlNoPad(rootKeyB64);
// Derive: SHA-512(rootKey || domain-separation-label)
final input = [
...rootKeyBytes,
...utf8.encode('|lk|$layerIndex|c|$cipherConstant'),
];
final hash = await Sha512().hash(input);
return hash.bytes.sublist(0, keyLength);
}
/// Decode a base64url string that may lack padding characters.
List<int> _decodeBase64UrlNoPad(String encoded) {
final padded = encoded + '=' * ((4 - encoded.length % 4) % 4);
return base64Url.decode(padded);
}
/// Constant-time MAC/hash comparison to prevent timing side-channels.
void _verifyMacBytes(List<int> expected, List<int> computed) {
if (expected.length != computed.length) {
throw ArgumentError('MAC verification failed');
}
int diff = 0;
for (int i = 0; i < expected.length; i++) {
diff |= expected[i] ^ computed[i];
}
if (diff != 0) {
throw ArgumentError('MAC verification failed');
}
}
/// HMAC-SHA512 authentication/verification.
///
/// Derived-key output: `[data][64B MAC]`.
/// Legacy output: `[64B key][data][64B MAC]`.
Future<List<int>> _executeHmacSha512(List<int> data, Map<String, dynamic> params, bool isEncrypt,
{int layerIndex = 0}) async {
final algorithm = Hmac.sha512();
final useDerivedKey = params.containsKey('phase1_root_key_b64');
if (isEncrypt) {
final SecretKey secretKey;
final List<int>? legacyKeyBytes;
if (useDerivedKey) {
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.HMAC_SHA512, 64);
secretKey = SecretKeyData(keyBytes);
legacyKeyBytes = null;
} else {
final keyBytes = List.generate(64, (i) => DateTime.now().microsecond % 256);
secretKey = SecretKeyData(keyBytes);
legacyKeyBytes = keyBytes;
}
final mac = await algorithm.calculateMac(data, secretKey: secretKey);
if (useDerivedKey) {
// Derived-key format: [data][mac]
return [...data, ...mac.bytes];
}
// Legacy format: [key][data][mac]
return [...legacyKeyBytes!, ...data, ...mac.bytes];
} else {
if (useDerivedKey) {
// Derived-key format: [data][64B mac]
if (data.length < 64) {
throw ArgumentError('Invalid HMAC-SHA512 derived-key data length');
}
final macBytes = data.sublist(data.length - 64);
final originalData = data.sublist(0, data.length - 64);
final keyBytes = await _deriveLayerKey(params, layerIndex, CipherConstants.HMAC_SHA512, 64);
final computedMac = await algorithm.calculateMac(originalData, secretKey: SecretKeyData(keyBytes));
_verifyMacBytes(macBytes, computedMac.bytes);
return originalData;
}
// Legacy format: [64B key][data][64B mac]
if (data.length < 64 + 64) {
throw ArgumentError('Invalid HMAC-SHA512 data length');
}
final keyBytes = data.sublist(0, 64);
final macBytes = data.sublist(data.length - 64);
final originalData = data.sublist(64, data.length - 64);
final computedMac = await algorithm.calculateMac(originalData, secretKey: SecretKeyData(keyBytes));
_verifyMacBytes(macBytes, computedMac.bytes);
return originalData;
}
}
/// BLAKE2b hashing/verification (keyless integrity check).
Future<List<int>> _executeBlake2b(List<int> data, Map<String, dynamic> params, bool isEncrypt) async {
final algorithm = Blake2b();
if (isEncrypt) {
final hash = await algorithm.hash(data);
return [...data, ...hash.bytes];
} else {
if (data.length < 64) {
throw ArgumentError('Invalid BLAKE2b data length');
}
final hashBytes = data.sublist(data.length - 64);
final originalData = data.sublist(0, data.length - 64);
final computedHash = await algorithm.hash(originalData);
_verifyMacBytes(hashBytes, computedHash.bytes);
return originalData;
}
}
// ---------------------------------------------------------------------------
// Length-Hiding Padding (Phase 3)
// ---------------------------------------------------------------------------
/// Default minimum random padding bytes.
const int _paddingDefaultMinBytes = 32;
/// Default block size for length-hiding alignment.
const int _paddingDefaultBlockSize = 256;
/// Apply length-hiding padding to plaintext before encryption.
///
/// Format: `[plaintext][random_padding][4-byte padding_length_LE]`
///
/// The padding ensures:
/// 1. At least [minPaddingBytes] random bytes are appended (defeats exact
/// length correlation even for known plaintexts).
/// 2. The total padded length is rounded up to the next multiple of
/// [blockSize] (groups all messages into fixed-size buckets so an
/// eavesdropper cannot distinguish "hi" from "hey").
///
/// The last 4 bytes always store the total padding length as a little-endian
/// uint32, allowing deterministic stripping on the receiver side.
///
/// When both `ccc_min_padding_bytes` and `ccc_block_size` are absent from
/// [params] (legacy channels), padding is still applied with conservative
/// defaults (32B min, 256B block) so all new encryptions are length-hidden.
List<int> _applyPadding(List<int> plaintext, Map<String, dynamic> params) {
final minPadding = (params['ccc_min_padding_bytes'] as int?) ?? _paddingDefaultMinBytes;
final blockSize = (params['ccc_block_size'] as int?) ?? _paddingDefaultBlockSize;
// 4 bytes reserved for the padding-length trailer
const trailerSize = 4;
// Minimum total size: plaintext + minPadding + trailer
final minTotal = plaintext.length + minPadding + trailerSize;
// Round up to next block boundary (block size of 0 or 1 means no alignment)
final effectiveBlock = blockSize > 1 ? blockSize : 1;
final paddedTotal = ((minTotal + effectiveBlock - 1) ~/ effectiveBlock) * effectiveBlock;
// Total padding = everything after plaintext
final totalPadding = paddedTotal - plaintext.length;
// Fill random padding bytes (all except last 4 which are the trailer)
final rng = Random.secure();
final randomLen = totalPadding - trailerSize;
final randomBytes = List<int>.generate(randomLen, (_) => rng.nextInt(256));
// Encode totalPadding as 4-byte little-endian
final trailer = [
totalPadding & 0xFF,
(totalPadding >> 8) & 0xFF,
(totalPadding >> 16) & 0xFF,
(totalPadding >> 24) & 0xFF,
];
return [...plaintext, ...randomBytes, ...trailer];
}
/// Strip length-hiding padding after decryption.
///
/// Reads the 4-byte little-endian trailer at the end of [paddedData] to
/// determine how many bytes of padding to remove.
///
/// Throws [ArgumentError] if the trailer claims a padding length larger than
/// the data itself (corruption or tamper indicator the AEAD layer should
/// already have caught tampering, but defense-in-depth).
List<int> _stripPadding(List<int> paddedData) {
const trailerSize = 4;
if (paddedData.length < trailerSize) {
// Data too short to contain a padding trailer return as-is for
// backward compatibility with pre-Phase-3 messages that have no padding.
return paddedData;
}
final len = paddedData.length;
final totalPadding = paddedData[len - 4] |
(paddedData[len - 3] << 8) |
(paddedData[len - 2] << 16) |
(paddedData[len - 1] << 24);
// Sanity: padding must be at least trailerSize and at most the full data
if (totalPadding < trailerSize || totalPadding > len) {
// Not a padded message (pre-Phase-3 legacy) return unchanged.
return paddedData;
}
return paddedData.sublist(0, len - totalPadding);
}

View File

@ -0,0 +1,247 @@
///
/// Crypto Isolate Manager
/// Manages worker pool for encryption/decryption operations
///
import 'dart:async';
import 'package:worker_manager/worker_manager.dart';
import 'ccc_iso_operation.dart';
import 'ccc_iso_result.dart';
import 'ccc_iso_operation_id.dart';
import 'ccc_iso.dart';
import 'cipher_constants.dart';
/// Manages isolate workers for cryptographic operations
///
/// CCC (Copius Cipher Chain) Architecture:
/// - Single encryption system for ALL data (messages, attachments, thumbnails)
/// - Uses channel UUID for key derivation scope
/// - Same key encrypts message + its attachments (bundled together)
/// - Used for both over-the-wire AND local storage encryption
///
/// FUTURE WORK: Ratchet System
/// - Per-message key derivation using Double Ratchet algorithm
/// - Keys derived from: channelUuid + messageSequence + rootKey
/// - Forward secrecy: compromise of one key doesn't reveal past messages
/// - Break-in recovery: new keys derived after compromise
///
/// Current State: Full CCC encryption with isolate workers
/// Future: Ratchet key derivation integration
class CryptoIsolateManager {
// Performance tracking
final CryptoMetrics _metrics = CryptoMetrics();
// Configuration
static const Duration OPERATION_TIMEOUT = Duration(seconds: 30);
static const int WORKER_POOL_SIZE = 4; // Fixed pool of 4 workers
bool _initialized = false;
bool _disposed = false;
/// Initialize the crypto isolate manager
Future<void> initialize() async {
if (_initialized) return;
// Initialize operation ID generator
CryptoOperationId.initialize();
// Configure worker manager with fixed pool size
await workerManager.init(isolatesCount: WORKER_POOL_SIZE);
_initialized = true;
}
/// Encrypt data with specified cipher sequence
/// In passthrough mode, returns data unchanged (for development)
///
/// [channelUuid] - Channel UUID for key derivation scope
/// [messageSequence] - Message sequence number for per-message key derivation (future: ratchet)
/// [isUserMessage] - Priority flag for worker queue
///
/// FUTURE: When ratchet is implemented, key = derive(channelRootKey, messageSequence)
/// Same key will encrypt: MsgData + all attachment files + all thumbnails for that message
Future<CryptoResult> encrypt(
List<int> plaintext, {
required String channelUuid,
int? messageSequence, // Future: used for ratchet key derivation
List<int>? cipherSequence,
Map<String, dynamic>? params,
bool isUserMessage = false,
}) async {
_ensureInitialized();
final operation = CryptoOperation.encrypt(
plaintext: plaintext,
cipherSequence: cipherSequence ?? CipherConstants.PHASE1_SEQUENCE,
params: params ?? CipherConstants.DEFAULT_CIPHER_PARAMS,
channelUuid: channelUuid,
messageSequence: messageSequence,
isUserMessage: isUserMessage,
);
return await _executeOperation(operation);
}
/// Decrypt data with specified cipher sequence
/// In passthrough mode, returns data unchanged (for development)
///
/// [channelUuid] - Channel UUID for key derivation scope
/// [messageSequence] - Message sequence number for per-message key derivation (future: ratchet)
///
/// FUTURE: When ratchet is implemented, key = derive(channelRootKey, messageSequence)
Future<CryptoResult> decrypt(
List<int> ciphertext, {
required String channelUuid,
int? messageSequence, // Future: used for ratchet key derivation
List<int>? cipherSequence,
Map<String, dynamic>? params,
}) async {
_ensureInitialized();
final operation = CryptoOperation.decrypt(
ciphertext: ciphertext,
cipherSequence: cipherSequence ?? CipherConstants.PHASE1_SEQUENCE,
params: params ?? CipherConstants.DEFAULT_CIPHER_PARAMS,
channelUuid: channelUuid,
messageSequence: messageSequence,
);
return await _executeOperation(operation);
}
/// Execute a crypto operation
Future<CryptoResult> _executeOperation(CryptoOperation operation) async {
_ensureInitialized();
if (_disposed) {
throw StateError('CryptoIsolateManager has been disposed');
}
try {
// Execute using the global workerManager with correct API
final resultMap = await workerManager.execute<Map<String, dynamic>>(
() => cryptoWorkerFunction(operation.toMap()),
priority: operation.priority,
).timeout(OPERATION_TIMEOUT);
// Deserialize result
final result = CryptoResult.fromMap(resultMap);
// Update metrics
_updateMetrics(operation, result);
return result;
} catch (error, stackTrace) {
// Create error result
final result = CryptoResult.error(
operationId: operation.id,
error: error.toString(),
stackTrace: stackTrace.toString(),
processingTime: operation.age,
workerId: 'manager_error',
);
// Update metrics
_metrics.recordError();
return result;
}
}
/// Update performance metrics
void _updateMetrics(CryptoOperation operation, CryptoResult result) {
if (result.success) {
if (operation.type == OperationType.encrypt) {
_metrics.recordEncryption(result.processingTime, operation.isHighPriority);
} else {
_metrics.recordDecryption(result.processingTime);
}
} else {
_metrics.recordError();
}
}
/// Get current performance metrics
CryptoMetrics get metrics => _metrics;
/// Get worker manager stats
String get workerStats {
return 'Crypto Manager Stats: ${_metrics.toString()}';
}
/// Check if manager is ready
bool get isReady => _initialized && !_disposed;
/// Ensure manager is initialized
void _ensureInitialized() {
if (!_initialized) {
throw StateError('CryptoIsolateManager must be initialized before use');
}
if (_disposed) {
throw StateError('CryptoIsolateManager has been disposed');
}
}
/// Dispose the isolate manager and clean up resources
Future<void> dispose() async {
if (_disposed) return;
_disposed = true;
// Dispose global worker manager
try {
await workerManager.dispose();
} catch (e) {
// Ignore disposal errors
}
// Clear metrics
_metrics.reset();
}
/// Create a test operation (for debugging)
Future<CryptoResult> testOperation({
String testData = 'Hello, Crypto World!',
String channelUuid = 'test-channel-uuid',
}) async {
final plaintext = testData.codeUnits;
// Encrypt
final encryptResult = await encrypt(
plaintext,
channelUuid: channelUuid,
isUserMessage: true,
);
if (!encryptResult.success) {
return encryptResult;
}
// Decrypt
final decryptResult = await decrypt(
encryptResult.data,
channelUuid: channelUuid,
);
if (!decryptResult.success) {
return decryptResult;
}
// Verify roundtrip
final decryptedText = String.fromCharCodes(decryptResult.data);
if (decryptedText == testData) {
return CryptoResult.success(
operationId: 'test_roundtrip',
data: decryptResult.data,
processingTime: encryptResult.processingTime + decryptResult.processingTime,
workerId: 'test_manager',
);
} else {
return CryptoResult.error(
operationId: 'test_roundtrip',
error: 'Roundtrip failed: expected "$testData", got "$decryptedText"',
processingTime: encryptResult.processingTime + decryptResult.processingTime,
workerId: 'test_manager',
);
}
}
}

View File

@ -0,0 +1,190 @@
///
/// Data structure for crypto operations in isolates
/// Handles serialization for isolate communication
///
import 'package:worker_manager/worker_manager.dart';
import 'ccc_iso_operation_id.dart';
import 'cipher_constants.dart';
/// Type of cryptographic operation
enum OperationType {
encrypt,
decrypt;
@override
String toString() => name;
static OperationType fromString(String value) {
return OperationType.values.firstWhere(
(type) => type.name == value,
orElse: () => throw ArgumentError('Invalid OperationType: $value'),
);
}
}
/// Cryptographic operation data for isolate workers
///
/// Key derivation (future ratchet implementation):
/// - channelUuid: Scope for key derivation (each channel has unique root key)
/// - messageSequence: Per-message key derivation index
/// - Final key = ratchet(channelRootKey, messageSequence)
///
/// Same derived key encrypts: MsgData + attachment files + thumbnails
class CryptoOperation {
final String id;
final List<int> data;
final List<int> cipherSequence;
final Map<String, dynamic> params;
final OperationType type;
final WorkPriority priority;
final String channelUuid; // Channel UUID for key derivation scope
final int? messageSequence; // Future: ratchet index for per-message key
final DateTime timestamp;
/// Create new crypto operation with auto-generated ID
CryptoOperation({
required this.data,
required this.cipherSequence,
required this.params,
required this.type,
required this.channelUuid,
this.messageSequence,
this.priority = WorkPriority.high,
}) : id = CryptoOperationId.generate(),
timestamp = DateTime.now();
/// Create crypto operation with custom ID (for testing)
CryptoOperation.withId({
required this.id,
required this.data,
required this.cipherSequence,
required this.params,
required this.type,
required this.channelUuid,
this.messageSequence,
this.priority = WorkPriority.high,
}) : timestamp = DateTime.now();
/// Create operation for encryption
factory CryptoOperation.encrypt({
required List<int> plaintext,
required List<int> cipherSequence,
required Map<String, dynamic> params,
required String channelUuid,
int? messageSequence,
bool isUserMessage = false,
}) {
return CryptoOperation(
data: plaintext,
cipherSequence: cipherSequence,
params: params,
type: OperationType.encrypt,
channelUuid: channelUuid,
messageSequence: messageSequence,
priority: isUserMessage ? WorkPriority.immediately : WorkPriority.high,
);
}
/// Create operation for decryption
factory CryptoOperation.decrypt({
required List<int> ciphertext,
required List<int> cipherSequence,
required Map<String, dynamic> params,
required String channelUuid,
int? messageSequence,
}) {
return CryptoOperation(
data: ciphertext,
cipherSequence: cipherSequence,
params: params,
type: OperationType.decrypt,
channelUuid: channelUuid,
messageSequence: messageSequence,
priority: WorkPriority.high,
);
}
/// Serialize for isolate communication
Map<String, dynamic> toMap() {
return {
'id': id,
'data': data,
'cipherSequence': cipherSequence,
'params': params,
'type': type.name,
'priority': priority.index,
'channelUuid': channelUuid,
'messageSequence': messageSequence,
'timestamp': timestamp.millisecondsSinceEpoch,
};
}
/// Deserialize from isolate communication
static CryptoOperation fromMap(Map<String, dynamic> map) {
return CryptoOperation.withId(
id: map['id'] as String,
data: List<int>.from(map['data'] as List),
cipherSequence: List<int>.from(map['cipherSequence'] as List),
params: Map<String, dynamic>.from(map['params'] as Map),
type: OperationType.fromString(map['type'] as String),
channelUuid: map['channelUuid'] as String,
messageSequence: map['messageSequence'] as int?,
priority: WorkPriority.values[map['priority'] as int],
);
}
/// Get cipher sequence as human-readable string
String get cipherSequenceDescription {
return CipherConstants.getSequenceDescription(cipherSequence);
}
/// Validate cipher sequence
bool get hasValidCipherSequence {
return CipherConstants.isValidSequence(cipherSequence);
}
/// Get data size in bytes
int get dataSize => data.length;
/// Get operation age
Duration get age => DateTime.now().difference(timestamp);
/// Check if operation is high priority
bool get isHighPriority => priority == WorkPriority.immediately;
/// Copy with new priority
CryptoOperation copyWithPriority(WorkPriority newPriority) {
return CryptoOperation.withId(
id: id,
data: data,
cipherSequence: cipherSequence,
params: params,
type: type,
channelUuid: channelUuid,
messageSequence: messageSequence,
priority: newPriority,
);
}
@override
String toString() {
return 'CryptoOperation('
'id: $id, '
'type: $type, '
'dataSize: ${dataSize}B, '
'ciphers: ${cipherSequence.length}, '
'priority: $priority, '
'channel: $channelUuid'
')';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CryptoOperation &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}

View File

@ -0,0 +1,96 @@
///
///
///
/// Fast sequence-based ID generator for crypto operations
/// Optimized for high-frequency crypto tasks with minimal overhead
class CryptoOperationId {
static int _counter = 0;
static late final String _sessionId;
static bool _initialized = false;
/// Initialize the ID system with a new session
static void initialize() {
if (_initialized) return; // Don't reinitialize
_sessionId = DateTime.now().millisecondsSinceEpoch.toString();
_counter = 0;
_initialized = true;
}
/// Generate next operation ID
/// Format: "timestamp_sequence"
/// Example: "1692123456789_1"
static String generate() {
if (!_initialized) {
initialize();
}
return '${_sessionId}_${++_counter}';
}
/// Get current session ID (for debugging)
static String get currentSession {
if (!_initialized) {
initialize();
}
return _sessionId;
}
/// Get total operations generated in this session
static int get operationCount => _counter;
/// Reset counter (useful for testing)
static void reset() {
_counter = 0;
}
/// Parse operation ID to extract session and sequence
static OperationIdInfo? parse(String operationId) {
final parts = operationId.split('_');
if (parts.length != 2) return null;
final sessionId = parts[0];
final sequence = int.tryParse(parts[1]);
if (sequence == null) return null;
return OperationIdInfo(
sessionId: sessionId,
sequence: sequence,
operationId: operationId,
);
}
}
/// Information extracted from an operation ID
class OperationIdInfo {
final String sessionId;
final int sequence;
final String operationId;
const OperationIdInfo({
required this.sessionId,
required this.sequence,
required this.operationId,
});
/// Get session timestamp
DateTime? get sessionTimestamp {
final timestamp = int.tryParse(sessionId);
if (timestamp == null) return null;
return DateTime.fromMillisecondsSinceEpoch(timestamp);
}
@override
String toString() => 'OperationIdInfo(session: $sessionId, sequence: $sequence)';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OperationIdInfo &&
runtimeType == other.runtimeType &&
sessionId == other.sessionId &&
sequence == other.sequence;
@override
int get hashCode => sessionId.hashCode ^ sequence.hashCode;
}

View File

@ -0,0 +1,220 @@
///
/// Result structure for crypto operations
/// Handles success/error states and performance metrics
///
/// Result of a cryptographic operation
class CryptoResult {
final String operationId;
final List<int> data;
final bool success;
final String? error;
final String? stackTrace;
final Duration processingTime;
final String workerId;
final DateTime completedAt;
/// Create successful result
CryptoResult.success({
required this.operationId,
required this.data,
required this.processingTime,
required this.workerId,
}) : success = true,
error = null,
stackTrace = null,
completedAt = DateTime.now();
/// Create error result
CryptoResult.error({
required this.operationId,
required this.error,
this.stackTrace,
required this.processingTime,
required this.workerId,
}) : success = false,
data = const [],
completedAt = DateTime.now();
/// Serialize for isolate communication
Map<String, dynamic> toMap() {
return {
'operationId': operationId,
'data': data,
'success': success,
'error': error,
'stackTrace': stackTrace,
'processingTimeMs': processingTime.inMilliseconds,
'workerId': workerId,
'completedAtMs': completedAt.millisecondsSinceEpoch,
};
}
/// Deserialize from isolate communication
static CryptoResult fromMap(Map<String, dynamic> map) {
final success = map['success'] as bool;
final processingTime = Duration(milliseconds: map['processingTimeMs'] as int);
final workerId = map['workerId'] as String;
final operationId = map['operationId'] as String;
if (success) {
return CryptoResult.success(
operationId: operationId,
data: List<int>.from(map['data'] as List),
processingTime: processingTime,
workerId: workerId,
);
} else {
return CryptoResult.error(
operationId: operationId,
error: map['error'] as String?,
stackTrace: map['stackTrace'] as String?,
processingTime: processingTime,
workerId: workerId,
);
}
}
/// Get result data size in bytes
int get dataSize => data.length;
/// Get processing speed in bytes per second
double get bytesPerSecond {
final seconds = processingTime.inMilliseconds / 1000.0;
if (seconds <= 0) return 0.0;
return dataSize / seconds;
}
/// Get human-readable processing speed
String get formattedSpeed {
final bps = bytesPerSecond;
if (bps >= 1024 * 1024) {
return '${(bps / (1024 * 1024)).toStringAsFixed(2)} MB/s';
} else if (bps >= 1024) {
return '${(bps / 1024).toStringAsFixed(2)} KB/s';
} else {
return '${bps.toStringAsFixed(2)} B/s';
}
}
/// Check if operation was slow
bool get isSlow => processingTime.inMilliseconds > 1000; // > 1 second
/// Check if operation was fast
bool get isFast => processingTime.inMilliseconds < 100; // < 100ms
@override
String toString() {
if (success) {
return 'CryptoResult.success('
'id: $operationId, '
'dataSize: ${dataSize}B, '
'time: ${processingTime.inMilliseconds}ms, '
'speed: $formattedSpeed, '
'worker: $workerId'
')';
} else {
return 'CryptoResult.error('
'id: $operationId, '
'error: $error, '
'time: ${processingTime.inMilliseconds}ms, '
'worker: $workerId'
')';
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CryptoResult &&
runtimeType == other.runtimeType &&
operationId == other.operationId &&
success == other.success;
@override
int get hashCode => operationId.hashCode ^ success.hashCode;
}
/// Performance metrics for crypto operations
class CryptoMetrics {
int encryptionCount = 0;
int decryptionCount = 0;
Duration totalEncryptionTime = Duration.zero;
Duration totalDecryptionTime = Duration.zero;
int errorCount = 0;
int priorityOperations = 0;
DateTime? lastOperationTime;
/// Record successful encryption
void recordEncryption(Duration time, bool wasPriority) {
encryptionCount++;
totalEncryptionTime += time;
if (wasPriority) priorityOperations++;
lastOperationTime = DateTime.now();
}
/// Record successful decryption
void recordDecryption(Duration time) {
decryptionCount++;
totalDecryptionTime += time;
lastOperationTime = DateTime.now();
}
/// Record error
void recordError() {
errorCount++;
lastOperationTime = DateTime.now();
}
/// Get total operations
int get totalOperations => encryptionCount + decryptionCount;
/// Get average encryption time
Duration get averageEncryptionTime {
if (encryptionCount == 0) return Duration.zero;
return Duration(milliseconds: totalEncryptionTime.inMilliseconds ~/ encryptionCount);
}
/// Get average decryption time
Duration get averageDecryptionTime {
if (decryptionCount == 0) return Duration.zero;
return Duration(milliseconds: totalDecryptionTime.inMilliseconds ~/ decryptionCount);
}
/// Get error rate (0.0 to 1.0)
double get errorRate {
final total = totalOperations + errorCount;
if (total == 0) return 0.0;
return errorCount / total;
}
/// Get priority operation percentage
double get priorityPercentage {
if (totalOperations == 0) return 0.0;
return priorityOperations / totalOperations;
}
/// Reset all metrics
void reset() {
encryptionCount = 0;
decryptionCount = 0;
totalEncryptionTime = Duration.zero;
totalDecryptionTime = Duration.zero;
errorCount = 0;
priorityOperations = 0;
lastOperationTime = null;
}
@override
String toString() {
return 'CryptoMetrics('
'operations: $totalOperations, '
'encryptions: $encryptionCount, '
'decryptions: $decryptionCount, '
'errors: $errorCount, '
'avgEncTime: ${averageEncryptionTime.inMilliseconds}ms, '
'avgDecTime: ${averageDecryptionTime.inMilliseconds}ms, '
'errorRate: ${(errorRate * 100).toStringAsFixed(1)}%'
')';
}
}

View File

@ -0,0 +1,40 @@
/// CCC Key-Derivation Function (KDF) definitions and helpers.
///
/// This module owns:
/// - stable int-backed KDF identifiers for persistence,
/// - normalization/parsing helpers,
/// - conversion from persisted int to enum.
///
/// Keeping this in `ccc/` preserves separation of concerns so data models remain
/// focused on storage shape only.
enum CccKdfFunction {
sha256(1),
sha384(2),
sha512(3),
blake2b512(4);
final int value;
const CccKdfFunction(this.value);
}
/// Default persisted KDF selector.
const int cccDefaultKdfFunctionValue = 1;
/// Normalize arbitrary input to a supported KDF function int value.
int normalizeCccKdfFunctionValue(dynamic raw) {
final parsed = raw is int ? raw : int.tryParse(raw?.toString() ?? '');
if (parsed == null) return cccDefaultKdfFunctionValue;
for (final option in CccKdfFunction.values) {
if (option.value == parsed) return parsed;
}
return cccDefaultKdfFunctionValue;
}
/// Resolve an enum value from persisted int representation.
CccKdfFunction cccKdfFunctionFromInt(int raw) {
for (final option in CccKdfFunction.values) {
if (option.value == raw) return option;
}
return CccKdfFunction.sha256;
}

View File

@ -0,0 +1,148 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
/// local imports
import 'package:letusmsg/ccc/ccc_kdf.dart';
import 'package:letusmsg/data/ccc_data.dart';
/// Phase 1 key schedule helper.
///
/// This utility derives deterministic channel-root and per-message key material
/// with a selectable KDF/hash option so message/attachment flows can share one
/// key-schedule contract before full ratchet rollout.
class CccKeySchedule {
static const String version = 'phase1-v1';
/// Derive the deterministic 32-byte root key for a channel.
///
/// This is the shared secret that all channel members can independently
/// compute from the channel UUID and CCCData configuration. Used both
/// by the Phase 1 static key schedule and as the seed for Phase 2
/// ratchet initialization via [RatchetEngine.initializeFromRoot].
///
/// Returns a 32-byte root key (truncated from the full hash output).
static Future<List<int>> deriveRootKey({
required String channelUuid,
required CCCData? cccData,
}) async {
final combo = cccData?.combo ?? 0;
final iterations = cccData?.iterrations ?? 0;
final kdfFunctionValue = normalizeCccKdfFunctionValue(
cccData?.kdfFunction,
);
final kdfFunction = cccKdfFunctionFromInt(kdfFunctionValue);
final fullHash = await _hashUtf8(
'lum|$version|root|$channelUuid|combo:$combo|iter:$iterations',
kdfFunction,
);
return fullHash.sublist(0, 32);
}
/// Build Phase 1 key-schedule params for one channel/message operation.
///
/// The returned params are additive and safe to merge with existing CCC params.
static Future<CccPhase1KeyMaterial> buildMessageParams({
required String channelUuid,
required CCCData? cccData,
required Map<String, dynamic> baseCipherParams,
required bool isUserMessage,
int? messageSequence,
}) async {
final combo = cccData?.combo ?? 0;
final iterations = cccData?.iterrations ?? 0;
final kdfFunctionValue = normalizeCccKdfFunctionValue(
cccData?.kdfFunction,
);
final kdfFunction = cccKdfFunctionFromInt(kdfFunctionValue);
final effectiveSequence = messageSequence ?? 0;
final rootKey = await _hashUtf8(
'lum|$version|root|$channelUuid|combo:$combo|iter:$iterations',
kdfFunction,
);
final direction = isUserMessage ? 'out' : 'in';
final messageKey = await _hashBytes(
[
...rootKey,
...utf8.encode('|msg|$direction|seq:$effectiveSequence'),
],
kdfFunction,
);
// Note: direction and message_sequence are included in the AD map for
// key-schedule metadata completeness, but are stripped by the isolate
// worker's _buildAssociatedDataBytes() before AEAD binding. This is
// intentional for Phase 1: the sender does not know the relay-assigned
// server_seq at encryption time, and direction differs between sender
// ('out') and receiver ('in'). Phase 2 ratchet will re-enable sequence
// binding once both sides share agreed-upon counters.
final associatedData = <String, dynamic>{
'v': version,
'channel_uuid': channelUuid,
'ccc_combo': combo,
'ccc_iterations': iterations,
'message_sequence': effectiveSequence,
'direction': direction,
'kdf_function': kdfFunction.name,
'kdf_function_value': kdfFunction.value,
};
return CccPhase1KeyMaterial(
effectiveMessageSequence: effectiveSequence,
cipherParams: {
...baseCipherParams,
'phase1_key_schedule_version': version,
'phase1_kdf_function': kdfFunction.name,
'phase1_kdf_function_value': kdfFunction.value,
'phase1_effective_sequence': effectiveSequence,
'phase1_root_key_b64': _toBase64UrlNoPad(rootKey),
'phase1_message_key_b64': _toBase64UrlNoPad(messageKey),
'phase1_associated_data': associatedData,
},
);
}
/// Hash UTF-8 text to bytes using selected KDF/hash function.
static Future<List<int>> _hashUtf8(
String value,
CccKdfFunction kdfFunction,
) async {
return _hashBytes(utf8.encode(value), kdfFunction);
}
/// Hash bytes to bytes using selected KDF/hash function.
static Future<List<int>> _hashBytes(
List<int> bytes,
CccKdfFunction kdfFunction,
) async {
switch (kdfFunction) {
case CccKdfFunction.sha256:
return (await Sha256().hash(bytes)).bytes;
case CccKdfFunction.sha384:
return (await Sha384().hash(bytes)).bytes;
case CccKdfFunction.sha512:
return (await Sha512().hash(bytes)).bytes;
case CccKdfFunction.blake2b512:
return (await Blake2b().hash(bytes)).bytes;
}
}
static String _toBase64UrlNoPad(List<int> bytes) {
return base64UrlEncode(bytes).replaceAll('=', '');
}
}
/// Derived key material metadata for Phase 1 message encryption scheduling.
class CccPhase1KeyMaterial {
/// Effective message sequence used for this derivation.
final int effectiveMessageSequence;
/// Parameters merged into CCC cipher params for runtime usage.
final Map<String, dynamic> cipherParams;
const CccPhase1KeyMaterial({
required this.effectiveMessageSequence,
required this.cipherParams,
});
}

View File

@ -0,0 +1,182 @@
///
/// CCC provider specification models.
///
import 'package:letusmsg/ccc/cipher_constants.dart';
/// Execution strategy for provider routing inside CCC.
enum CccExecutionMode {
strict,
efficient,
auto,
}
CccExecutionMode cccExecutionModeFromString(String raw) {
switch (raw.trim().toLowerCase()) {
case 'strict':
return CccExecutionMode.strict;
case 'efficient':
return CccExecutionMode.efficient;
case 'auto':
return CccExecutionMode.auto;
default:
throw ArgumentError('Unsupported CCC execution mode: $raw');
}
}
/// Logical provider identities used by CCC channel profiles.
enum CccCryptoProvider {
cryptography,
wolfssl,
openssl,
boringssl,
}
/// Provider entry in a CCC provider chain.
///
/// Each provider advertises the ciphers it contributes to the channel plan.
class CccProviderSpec {
final CccCryptoProvider provider;
final List<int> ciphers;
const CccProviderSpec({
required this.provider,
required this.ciphers,
});
Map<String, dynamic> toMap() {
return {
'provider': provider.name,
'ciphers': ciphers,
};
}
}
/// Per-provider, per-cipher capability metadata.
class CccCipherCapability {
/// Whether the provider is currently available in this codebase/runtime.
///
/// `true` means the provider can be selected as primary in `auto` mode.
/// `false` means it can still appear in route planning metadata but should
/// not be chosen as active primary until availability changes.
final bool available;
/// Whether input/output format is identical across providers for this cipher.
///
/// This guards fallback safety. If this is `false`, fallback should be
/// treated as incompatible for that cipher unless a migration adapter exists.
final bool deterministicIo;
/// Relative efficiency score from 0..100 (higher = faster/cheaper).
///
/// Used by `efficient` mode to rank candidate providers for a cipher.
final int efficiencyScore;
/// Relative reliability score from 0..100 (higher = more trusted/stable).
///
/// Used as tie-breaker when efficiency scores are equal.
final int reliabilityScore;
/// Creates immutable provider capability metadata used by CCC route planning.
///
/// Parameter meanings:
/// - [available]: runtime/provider readiness flag.
/// - [deterministicIo]: indicates safe cross-provider fallback compatibility.
/// - [efficiencyScore]: performance rank (0..100), higher is better.
/// - [reliabilityScore]: confidence rank (0..100), higher is better.
const CccCipherCapability({
required this.available,
required this.deterministicIo,
required this.efficiencyScore,
required this.reliabilityScore,
});
}
/// Runtime/provider capability registry used by CCC route planning.
class CccProviderCatalog {
/// Placeholder score used until benchmark pipeline is implemented.
///
/// TODO(j3g): Replace with measured values from automated perf/reliability
/// benchmark suite per provider/cipher.
///
/// Example efficiency calculation (normalized 0..100):
/// efficiency = normalize(throughputMBps / latencyMs)
///
/// Example reliability calculation (normalized 0..100):
/// reliability = normalize((1 - errorRate) * 100 - crashPenalty - cvePenalty)
static const int pendingEfficiencyScore = 50;
static const int pendingReliabilityScore = 50;
static const CccCipherCapability _availableDeterministicPending = CccCipherCapability(
available: true,
deterministicIo: true,
efficiencyScore: pendingEfficiencyScore,
reliabilityScore: pendingReliabilityScore,
);
static const CccCipherCapability _unavailableDeterministicPending = CccCipherCapability(
available: false,
deterministicIo: true,
efficiencyScore: pendingEfficiencyScore,
reliabilityScore: pendingReliabilityScore,
);
static const Map<CccCryptoProvider, Map<int, CccCipherCapability>> capabilities = {
CccCryptoProvider.cryptography: {
CipherConstants.AES_GCM_256: _availableDeterministicPending,
CipherConstants.CHACHA20_POLY1305: _availableDeterministicPending,
CipherConstants.XCHACHA20_POLY1305: _availableDeterministicPending,
CipherConstants.HMAC_SHA512: _availableDeterministicPending,
CipherConstants.BLAKE2B: _availableDeterministicPending,
},
CccCryptoProvider.wolfssl: {
CipherConstants.AES_GCM_256: _unavailableDeterministicPending,
CipherConstants.CHACHA20_POLY1305: _unavailableDeterministicPending,
CipherConstants.XCHACHA20_POLY1305: _unavailableDeterministicPending,
CipherConstants.HMAC_SHA512: _unavailableDeterministicPending,
CipherConstants.BLAKE2B: _unavailableDeterministicPending,
},
CccCryptoProvider.openssl: {
CipherConstants.AES_GCM_256: _unavailableDeterministicPending,
CipherConstants.CHACHA20_POLY1305: _unavailableDeterministicPending,
CipherConstants.HMAC_SHA512: _unavailableDeterministicPending,
CipherConstants.BLAKE2B: _unavailableDeterministicPending,
},
CccCryptoProvider.boringssl: {
CipherConstants.AES_GCM_256: _unavailableDeterministicPending,
CipherConstants.CHACHA20_POLY1305: _unavailableDeterministicPending,
CipherConstants.XCHACHA20_POLY1305: _unavailableDeterministicPending,
CipherConstants.HMAC_SHA512: _unavailableDeterministicPending,
CipherConstants.BLAKE2B: _unavailableDeterministicPending,
},
};
static CccCipherCapability? capability(CccCryptoProvider provider, int cipher) {
return capabilities[provider]?[cipher];
}
static bool supports(CccCryptoProvider provider, int cipher) {
return capability(provider, cipher) != null;
}
static List<CccCryptoProvider> providersSupporting(
int cipher, {
bool availableOnly = true,
}) {
final ranked = <({CccCryptoProvider provider, CccCipherCapability capability})>[];
for (final entry in capabilities.entries) {
final cap = entry.value[cipher];
if (cap == null) continue;
if (availableOnly && !cap.available) continue;
ranked.add((provider: entry.key, capability: cap));
}
ranked.sort((a, b) {
final perf = b.capability.efficiencyScore.compareTo(a.capability.efficiencyScore);
if (perf != 0) return perf;
return b.capability.reliabilityScore.compareTo(a.capability.reliabilityScore);
});
return ranked.map((item) => item.provider).toList(growable: false);
}
}

View File

@ -0,0 +1,209 @@
///
/// Cipher Constants for Copious Cipher Chain
/// Integer constants for efficient cipher sequence storage and processing
///
class CipherConstants {
// Key Derivation Functions
static const int ARGON2ID = 1;
static const int PBKDF2 = 2;
static const int HKDF = 3;
static const int HCHACHA20 = 4;
// Symmetric Ciphers (AEAD)
static const int AES_GCM_128 = 10;
static const int AES_GCM_192 = 11;
static const int AES_GCM_256 = 12;
static const int CHACHA20_POLY1305 = 13;
static const int XCHACHA20_POLY1305 = 14;
// Symmetric Ciphers (Non-AEAD)
static const int AES_CBC_128 = 20;
static const int AES_CBC_192 = 21;
static const int AES_CBC_256 = 22;
static const int AES_CTR_128 = 23;
static const int AES_CTR_192 = 24;
static const int AES_CTR_256 = 25;
static const int CHACHA20 = 26;
static const int XCHACHA20 = 27;
// MAC Algorithms
static const int HMAC_SHA256 = 30;
static const int HMAC_SHA384 = 31;
static const int HMAC_SHA512 = 32;
static const int BLAKE2B = 33;
static const int BLAKE2S = 34;
static const int POLY1305 = 35;
// Hash Algorithms (for integrity verification)
static const int SHA256 = 40;
static const int SHA384 = 41;
static const int SHA512 = 42;
static const int BLAKE2B_HASH = 43;
static const int BLAKE2S_HASH = 44;
// Phase 1 Default Cipher Sequence (5 layers - Argon2id removed for proper reversibility)
static const List<int> PHASE1_SEQUENCE = [
AES_GCM_256, // Primary AEAD encryption
CHACHA20_POLY1305, // Stream cipher AEAD
XCHACHA20_POLY1305, // Extended nonce AEAD
HMAC_SHA512, // Additional authentication
BLAKE2B, // Final integrity check
];
// Complete sequence with key derivation (for future use)
static const List<int> PHASE1_COMPLETE_SEQUENCE = [
ARGON2ID, // Key strengthening (one-way - use for key derivation only)
AES_GCM_256, // Primary AEAD encryption
CHACHA20_POLY1305, // Stream cipher AEAD
XCHACHA20_POLY1305, // Extended nonce AEAD
HMAC_SHA512, // Additional authentication
BLAKE2B, // Final integrity check
];
// --- Basic / user-selectable cipher sequences (combo 5-9) ----------------
/// Single-layer AES-256-GCM (combo 5).
static const List<int> BASIC_AES_SEQUENCE = [AES_GCM_256];
/// Single-layer ChaCha20-Poly1305 (combo 6).
static const List<int> BASIC_CHACHA_SEQUENCE = [CHACHA20_POLY1305];
/// Single-layer XChaCha20-Poly1305 (combo 7).
static const List<int> BASIC_XCHACHA_SEQUENCE = [XCHACHA20_POLY1305];
/// Dual AEAD: AES-256-GCM + ChaCha20-Poly1305 (combo 8).
static const List<int> DUAL_AEAD_SEQUENCE = [AES_GCM_256, CHACHA20_POLY1305];
/// Triple AEAD: AES + ChaCha20 + XChaCha20 (combo 9).
static const List<int> TRIPLE_AEAD_SEQUENCE = [AES_GCM_256, CHACHA20_POLY1305, XCHACHA20_POLY1305];
// Default cipher parameters
static const Map<String, dynamic> DEFAULT_CIPHER_PARAMS = {
// Argon2id parameters
'argon2_memory': 64 * 1024, // 64 MB
'argon2_parallelism': 4, // 4 CPU cores
'argon2_iterations': 3, // 3 iterations
'argon2_hash_length': 32, // 256-bit output
// AES parameters
'aes_key_size': 256, // 256-bit keys
'aes_nonce_size': 12, // 96-bit nonces for GCM
// ChaCha parameters
'chacha_nonce_size': 12, // 96-bit nonces
'xchacha_nonce_size': 24, // 192-bit nonces
// HMAC parameters
'hmac_key_size': 64, // 512-bit keys
// BLAKE2B parameters
'blake2b_hash_size': 64, // 512-bit hashes
};
// Cipher name mapping for debugging
static const Map<int, String> CIPHER_NAMES = {
// Key Derivation
ARGON2ID: 'Argon2id',
PBKDF2: 'PBKDF2',
HKDF: 'HKDF',
HCHACHA20: 'HChaCha20',
// AEAD Ciphers
AES_GCM_128: 'AES-128-GCM',
AES_GCM_192: 'AES-192-GCM',
AES_GCM_256: 'AES-256-GCM',
CHACHA20_POLY1305: 'ChaCha20-Poly1305',
XCHACHA20_POLY1305: 'XChaCha20-Poly1305',
// Non-AEAD Ciphers
AES_CBC_128: 'AES-128-CBC',
AES_CBC_192: 'AES-192-CBC',
AES_CBC_256: 'AES-256-CBC',
AES_CTR_128: 'AES-128-CTR',
AES_CTR_192: 'AES-192-CTR',
AES_CTR_256: 'AES-256-CTR',
CHACHA20: 'ChaCha20',
XCHACHA20: 'XChaCha20',
// MAC Algorithms
HMAC_SHA256: 'HMAC-SHA256',
HMAC_SHA384: 'HMAC-SHA384',
HMAC_SHA512: 'HMAC-SHA512',
BLAKE2B: 'BLAKE2b',
BLAKE2S: 'BLAKE2s',
POLY1305: 'Poly1305',
// Hash Algorithms
SHA256: 'SHA256',
SHA384: 'SHA384',
SHA512: 'SHA512',
BLAKE2B_HASH: 'BLAKE2b-Hash',
BLAKE2S_HASH: 'BLAKE2s-Hash',
};
/// Get human-readable name for cipher constant
static String getCipherName(int cipherConstant) {
return CIPHER_NAMES[cipherConstant] ?? 'Unknown Cipher ($cipherConstant)';
}
/// Get human-readable sequence description
static String getSequenceDescription(List<int> sequence) {
return sequence.map((cipher) => getCipherName(cipher)).join(' -> ');
}
/// Validate cipher sequence
static bool isValidSequence(List<int> sequence) {
// Empty sequence is valid it represents the plaintext/legacy combo 0.
if (sequence.isEmpty) return true;
// Check all ciphers are known
for (final cipher in sequence) {
if (!CIPHER_NAMES.containsKey(cipher)) {
return false;
}
}
return true;
}
// --- Combo metadata -------------------------------------------------------
/// Human-readable names for each combo value.
///
/// Combos 0-4 are multi-layer / multi-provider configurations.
/// Combos 5-9 are user-selectable "basic" through "triple AEAD" options.
static const Map<int, String> COMBO_NAMES = {
0: 'Plaintext (legacy / unencrypted)',
1: 'Multi-Provider: wolfSSL + CCC',
2: 'Multi-Provider: BoringSSL + CCC',
3: 'Multi-Provider: OpenSSL + wolfSSL + CCC',
4: 'Multi-Provider: wolfSSL + OpenSSL + CCC',
5: 'Basic: AES-256-GCM',
6: 'Basic: ChaCha20-Poly1305',
7: 'Basic: XChaCha20-Poly1305',
8: 'Dual AEAD: AES + ChaCha20',
9: 'Triple AEAD: AES + ChaCha20 + XChaCha20',
};
/// Cipher sequence for each combo value.
static const Map<int, List<int>> COMBO_SEQUENCES = {
0: [], // plaintext / legacy empty sequence = no cipher layers
5: BASIC_AES_SEQUENCE,
6: BASIC_CHACHA_SEQUENCE,
7: BASIC_XCHACHA_SEQUENCE,
8: DUAL_AEAD_SEQUENCE,
9: TRIPLE_AEAD_SEQUENCE,
};
/// Maximum supported combo value.
static const int MAX_COMBO = 9;
/// Whether [combo] is a valid, known combo value.
static bool isValidCombo(int combo) => COMBO_NAMES.containsKey(combo);
/// Get the human-readable name for a combo, or a fallback string.
static String getComboName(int combo) {
return COMBO_NAMES[combo] ?? 'Unknown Combo ($combo)';
}
}

View File

@ -0,0 +1,116 @@
///
///
///
import 'ccc_iso_manager.dart';
import 'ccc_iso_result.dart';
import 'cipher_constants.dart';
/// Copious Cipher Chain - Phase 1 with Isolate Support
/// Now supports async operations using CryptoIsolateManager
class CopiousCipherChain {
final CryptoIsolateManager? _isolateManager; // New isolate-based crypto
final List<int> _cipherSequence; // Integer-based cipher sequence
final Map<String, dynamic> _cipherParams; // Cipher parameters
/// New constructor with isolate manager (Phase 1)
CopiousCipherChain.withIsolateManager({
required CryptoIsolateManager isolateManager,
List<int>? cipherSequence,
Map<String, dynamic>? cipherParams,
}) : _isolateManager = isolateManager,
_cipherSequence = cipherSequence ?? CipherConstants.PHASE1_SEQUENCE,
_cipherParams = cipherParams ?? CipherConstants.DEFAULT_CIPHER_PARAMS;
/// Async encrypt with isolate support
Future<List<int>> encryptAsync(
List<int> data, {
required String channelUuid,
int? messageSequence,
bool isUserMessage = false,
List<int>? customCipherSequence,
Map<String, dynamic>? customParams,
}) async {
if (_isolateManager == null) {
throw StateError('Isolate manager not available. Use legacy encrypt() or create with withIsolateManager()');
}
final result = await _isolateManager.encrypt(
data,
channelUuid: channelUuid,
messageSequence: messageSequence,
cipherSequence: customCipherSequence ?? _cipherSequence,
params: customParams ?? _cipherParams,
isUserMessage: isUserMessage,
);
if (!result.success) {
throw Exception('Encryption failed: ${result.error}');
}
return result.data;
}
/// Async decrypt with isolate support
Future<List<int>> decryptAsync(
List<int> data, {
required String channelUuid,
int? messageSequence,
List<int>? customCipherSequence,
Map<String, dynamic>? customParams,
}) async {
if (_isolateManager == null) {
throw StateError('Isolate manager not available. Use legacy decrypt() or create with withIsolateManager()');
}
final result = await _isolateManager.decrypt(
data,
channelUuid: channelUuid,
messageSequence: messageSequence,
cipherSequence: customCipherSequence ?? _cipherSequence,
params: customParams ?? _cipherParams,
);
if (!result.success) {
throw Exception('Decryption failed: ${result.error}');
}
return result.data;
}
/// Test the cipher chain with a roundtrip operation
Future<bool> testCipherChain({
String testData = 'Test message for cipher chain validation',
String channelUuid = 'test-channel-999',
}) async {
if (_isolateManager == null) {
return false; // Cannot test without isolate manager
}
try {
final result = await _isolateManager.testOperation(
testData: testData,
channelUuid: channelUuid,
);
return result.success;
} catch (e) {
return false;
}
}
/// Get cipher sequence description
String get cipherSequenceDescription {
return CipherConstants.getSequenceDescription(_cipherSequence);
}
/// Get performance metrics (if using isolate manager)
CryptoMetrics? get metrics => _isolateManager?.metrics;
/// Check if using isolate manager
bool get usesIsolateManager => _isolateManager != null;
/// Get current cipher sequence
List<int> get cipherSequence => List.from(_cipherSequence);
/// Get current cipher parameters
Map<String, dynamic> get cipherParams => Map.from(_cipherParams);
}

View File

@ -0,0 +1,69 @@
///
/// Core crypto contract used by message/attachment pipelines.
///
/// Generic encryption/decryption contract.
///
/// Implementations include:
/// - plaintext/testing providers,
/// - CCC isolate-backed providers,
/// - native FFI-backed providers.
abstract class CryptoAbstract<T> {
/// Human readable provider name for logs/diagnostics.
String get providerName;
/// True when provider does not apply real cryptographic protection.
bool get isPlaintextMode;
/// Encrypt typed input into bytes.
Future<List<int>> encrypt(T input, {CryptoContext? context});
/// Decrypt bytes back to typed data.
Future<T> decrypt(List<int> data, {CryptoContext? context});
}
/// Per-operation context for crypto providers.
///
/// Providers that require channel-scoped or message-scoped key derivation
/// can consume these values. Plaintext/testing providers can ignore them.
class CryptoContext {
/// Stable channel UUID used for channel-scoped keying.
final String? channelUuid;
/// Optional message sequence used for per-message key derivation.
final int? messageSequence;
/// Priority hint for provider internals.
final bool isUserMessage;
/// Optional channel-derived cipher sequence override.
final List<int>? cipherSequence;
/// Optional channel-derived cipher params override.
final Map<String, dynamic>? cipherParams;
const CryptoContext({
this.channelUuid,
this.messageSequence,
this.isUserMessage = false,
this.cipherSequence,
this.cipherParams,
});
}
CryptoProviderMode cryptoProviderModeFromString(String raw) {
final value = raw.trim().toLowerCase();
switch (value) {
case 'plaintext':
return CryptoProviderMode.plaintext;
case 'ccc':
return CryptoProviderMode.ccc;
default:
throw ArgumentError('Unsupported crypto provider mode: $raw');
}
}
enum CryptoProviderMode {
plaintext,
ccc,
}

View File

@ -0,0 +1,81 @@
///
/// CCC isolate-backed crypto provider for message payloads.
///
import 'dart:convert';
import 'package:letusmsg/ccc/ccc_iso_manager.dart';
import 'package:letusmsg/ccc/crypto_abstract.dart';
/// Real crypto provider backed by `CryptoIsolateManager`.
///
/// Notes:
/// - Requires `context.channelUuid` for channel-scoped operation.
/// - Uses JSON map serialization for current message payload format.
class CryptoCcc implements CryptoAbstract<Map<String, dynamic>> {
final CryptoIsolateManager _manager;
bool _initialized = false;
CryptoCcc(this._manager);
@override
String get providerName => 'CryptoCcc';
@override
bool get isPlaintextMode => false;
Future<void> _ensureInitialized() async {
if (_initialized) return;
await _manager.initialize();
_initialized = true;
}
String _requireChannelUuid(CryptoContext? context) {
final channelUuid = context?.channelUuid;
if (channelUuid == null || channelUuid.isEmpty) {
throw ArgumentError('CryptoCcc requires a non-empty channelUuid in CryptoContext');
}
return channelUuid;
}
@override
Future<List<int>> encrypt(Map<String, dynamic> input, {CryptoContext? context}) async {
await _ensureInitialized();
final channelUuid = _requireChannelUuid(context);
final plainBytes = utf8.encode(jsonEncode(input));
final result = await _manager.encrypt(
plainBytes,
channelUuid: channelUuid,
messageSequence: context?.messageSequence,
cipherSequence: context?.cipherSequence,
params: context?.cipherParams,
isUserMessage: context?.isUserMessage ?? false,
);
if (!result.success) {
throw Exception('CryptoCcc encrypt failed: ${result.error ?? 'unknown error'}');
}
return result.data;
}
@override
Future<Map<String, dynamic>> decrypt(List<int> data, {CryptoContext? context}) async {
await _ensureInitialized();
final channelUuid = _requireChannelUuid(context);
final result = await _manager.decrypt(
data,
channelUuid: channelUuid,
messageSequence: context?.messageSequence,
cipherSequence: context?.cipherSequence,
params: context?.cipherParams,
);
if (!result.success) {
throw Exception('CryptoCcc decrypt failed: ${result.error ?? 'unknown error'}');
}
return jsonDecode(utf8.decode(result.data));
}
}

View File

@ -0,0 +1,13 @@
///
/// Preferred plaintext provider naming.
///
import 'package:letusmsg/ccc/enc_dec_json.dart';
/// Plaintext/testing provider alias.
///
/// Keeps compatibility with existing EncDecJson behavior while making
/// provider intent explicit in pipeline wiring.
class CryptoPlaintext extends EncDecJson {
@override
String get providerName => 'CryptoPlaintext';
}

View File

@ -0,0 +1,26 @@
///
/// Native wolfSSL provider scaffold.
///
import 'package:letusmsg/ccc/crypto_abstract.dart';
/// Phase-0 scaffold for future wolfSSL FFI integration.
///
/// This provider is intentionally not implemented yet. It exists so pipeline
/// wiring and provider selection can be completed before FFI delivery.
class CryptoWolfSsl implements CryptoAbstract<Map<String, dynamic>> {
@override
String get providerName => 'CryptoWolfSsl';
@override
bool get isPlaintextMode => false;
@override
Future<List<int>> encrypt(Map<String, dynamic> input, {CryptoContext? context}) {
throw UnimplementedError('CryptoWolfSsl.encrypt is not implemented yet (Phase 3)');
}
@override
Future<Map<String, dynamic>> decrypt(List<int> data, {CryptoContext? context}) {
throw UnimplementedError('CryptoWolfSsl.decrypt is not implemented yet (Phase 3)');
}
}

View File

@ -0,0 +1,43 @@
///
/// Enc/Dec JSON implementation
///
import 'dart:convert';
import 'package:letusmsg/ccc/crypto_abstract.dart';
/// Plaintext JSON codec used for testing/development flows.
///
/// This class intentionally does not provide cryptographic protection.
class EncDecJson implements CryptoAbstract<Map<String, dynamic>> {
@override
String get providerName => 'EncDecJson';
@override
bool get isPlaintextMode => true;
// @override
// List<int> pre(String input) {
// return utf8.encode(input);
// }
@override
Future<List<int>> encrypt(Map<String, dynamic> data, {CryptoContext? context}) async {
// Simulate encryption delay
// await Future.delayed(Duration(milliseconds: 100));
return utf8.encode( jsonEncode(data) );
}
@override
Future<Map<String, dynamic>> decrypt(List<int> encdata, {CryptoContext? context}) async {
// Simulate decryption delay
// await Future.delayed(Duration(milliseconds: 100));
return jsonDecode(utf8.decode(encdata));
}
// @override
// String post(List<int> output) {
// return utf8.decode(output);
// }
}

View File

@ -0,0 +1,497 @@
///
/// Ratchet Engine Double Ratchet Core for Normal Mode
///
/// Pure-logic class with static methods that operate on [RatchetState] +
/// [CCCData]. Designed to run on the background isolate alongside the
/// existing CCC cipher chain.
///
/// Phase 2 of the Hydra Apocalypse Ratchet.
///
/// ## Per-Sender Chain Separation
///
/// Each participant holds a separate [RatchetState] per sender in the
/// channel. The chain key is derived as:
///
/// ```text
/// rootKey
/// KDF("chain|channelUuid|senderIdentifier") initial chainKey
///
/// For each message:
/// chainKey = KDF(chainKey, "next|channelUuid") // symmetric ratchet step
/// messageKey = KDF(chainKey, counter + channelUuid) // per-message key
/// ```
///
/// Including `senderIdentifier` in the initial derivation guarantees that
/// two senders sharing the same root key and channel produce different
/// chains, preventing key reuse when both send at the same counter.
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:letusmsg/ccc/ccc_kdf.dart';
import 'package:letusmsg/data/ccc_data.dart';
import 'package:letusmsg/data/ratchet_state_data.dart';
/// Result of a ratchet advance operation.
///
/// Contains the derived 32-byte base message key and the message counter
/// it corresponds to.
class RatchetAdvanceResult {
/// The derived 32-byte base message key for AEAD encryption/decryption.
///
/// This key is passed into the isolate worker as the root key for
/// per-layer key derivation (replacing the Phase 1 static root key).
final List<int> baseMessageKey;
/// The message counter this key corresponds to.
final int counter;
/// Ephemeral X25519 public key bytes (32 bytes) when an asymmetric ratchet
/// step was performed on this message. The caller must embed this in the
/// encrypted payload (``rk`` field) so receivers can apply the same reset.
///
/// Null when no asymmetric ratchet step was triggered.
final List<int>? asymmetricRatchetPublicKey;
const RatchetAdvanceResult({
required this.baseMessageKey,
required this.counter,
this.asymmetricRatchetPublicKey,
});
}
/// Core ratchet engine implementing the symmetric Double Ratchet.
///
/// All methods are static and pure they take [RatchetState] and [CCCData]
/// as input, mutate the state in place, and return derived key material.
///
/// The engine does **not** handle persistence; callers must save the updated
/// [RatchetState] via [RatchetStateManager] after each operation.
///
/// ## Key Derivation Chain
///
/// ```text
/// rootKey
/// KDF("chain|channelUuid|senderIdentifier") initial chainKey
///
/// For each message:
/// chainKey = KDF(chainKey, "next|channelUuid") // symmetric ratchet step
/// messageKey = KDF(chainKey, counter + channelUuid) // per-message key
/// ```
class RatchetEngine {
/// Default checkpoint interval (messages between chain key snapshots).
static const int defaultCheckpointInterval = 500;
/// Default asymmetric ratchet frequency (messages between KEM steps).
static const int defaultRatchetFrequency = 50;
// ---------------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------------
/// Initialize ratchet state from a shared root secret.
///
/// Called once when a channel is created or an invite is accepted.
/// All parties must call this with the same [rootKey] to establish
/// identical starting states critical for N-party group channels.
///
/// Each participant creates one state per sender (including themselves).
/// The [senderIdentifier] is mixed into the chain derivation so that
/// two senders sharing the same root and channel produce **different**
/// chain keys preventing key reuse when both send the same counter.
///
/// The [rootKey] should be a 32-byte cryptographically random secret
/// shared securely during the invite flow.
///
/// Returns a fully initialized [RatchetState] with:
/// - A chain key unique to this (channel, sender) pair
/// - Counters at 0
/// - Genesis checkpoint at counter 0
static Future<RatchetState> initializeFromRoot({
required List<int> rootKey,
required String channelUuid,
required String senderIdentifier,
required CCCData cccData,
}) async {
// Derive the chain key from root with sender-specific domain separation.
// Including senderIdentifier ensures that two participants sharing the
// same rootKey + channelUuid produce different chains, eliminating any
// key-reuse risk when both send at the same counter value.
final chainKey = await _kdf(
cccData,
rootKey,
'chain|$channelUuid|$senderIdentifier',
);
final state = RatchetState(
encryptedRootKey: List<int>.from(rootKey), // TODO: encrypt under device master key
chainKey: chainKey,
channelUuid: channelUuid,
senderIdentifier: senderIdentifier,
);
// Genesis checkpoint
state.checkpoints[0] = List<int>.from(chainKey);
return state;
}
// ---------------------------------------------------------------------------
// Symmetric Chain Advance
// ---------------------------------------------------------------------------
/// Advance the chain and derive the base message key for sending.
///
/// This is the "click" of the ratchet. When
/// [CCCData.symmetricRatchetEveryMessage] is true (default), the chain key
/// is replaced with its KDF successor and the old value is permanently lost.
///
/// The returned [RatchetAdvanceResult.baseMessageKey] is a unique 32-byte
/// key for this specific message, suitable for passing into the isolate
/// worker as the root key for per-layer AEAD derivation.
///
/// **Side effects**: mutates [state] (chain key, counter, checkpoints).
/// Caller must persist the updated state.
static Future<RatchetAdvanceResult> advanceSending({
required RatchetState state,
required CCCData cccData,
}) async {
final counter = state.lastSentCounter + 1;
// Symmetric ratchet step: derive new chain key from current
if (cccData.symmetricRatchetEveryMessage) {
state.chainKey = await _kdf(
cccData,
state.chainKey,
'next|${state.channelUuid}',
);
}
// Derive per-message base key
final baseMessageKey = await _deriveMessageKey(
cccData,
state.chainKey,
counter,
state.channelUuid,
);
// Update counter
state.lastSentCounter = counter;
// Checkpoint if needed
_maybeCheckpoint(
state.checkpoints,
counter,
state.chainKey,
cccData.checkpointInterval,
);
// Asymmetric ratchet step: generates fresh X25519 keypair and mixes
// entropy into chain. The returned public key must be embedded in the
// message payload ("rk" field) so receivers can apply the same reset.
List<int>? asymmetricPublicKey;
if (_isAsymmetricRatchetDue(counter, cccData.ratchetFrequency)) {
asymmetricPublicKey = await performAsymmetricRatchet(
state: state,
cccData: cccData,
);
}
return RatchetAdvanceResult(
baseMessageKey: baseMessageKey,
counter: counter,
asymmetricRatchetPublicKey: asymmetricPublicKey,
);
}
/// Advance the chain to match the incoming message counter
/// and derive the base message key for decryption.
///
/// The chain is fast-forwarded from its current position to
/// [incomingCounter], applying the symmetric ratchet step at each
/// intermediate position.
///
/// **Side effects**: mutates [state] (chain key, counter, checkpoints).
/// Caller must persist the updated state.
static Future<RatchetAdvanceResult> advanceReceiving({
required RatchetState state,
required CCCData cccData,
required int incomingCounter,
}) async {
// Fast-forward chain to match incoming counter
while (state.lastReceivedCounter < incomingCounter) {
if (cccData.symmetricRatchetEveryMessage) {
state.chainKey = await _kdf(
cccData,
state.chainKey,
'next|${state.channelUuid}',
);
}
state.lastReceivedCounter += 1;
// Checkpoint at intermediate steps too
_maybeCheckpoint(
state.checkpoints,
state.lastReceivedCounter,
state.chainKey,
cccData.checkpointInterval,
);
}
// Derive per-message base key
final baseMessageKey = await _deriveMessageKey(
cccData,
state.chainKey,
incomingCounter,
state.channelUuid,
);
return RatchetAdvanceResult(
baseMessageKey: baseMessageKey,
counter: incomingCounter,
);
}
// ---------------------------------------------------------------------------
// Old Message Decryption (Random Access via Checkpoints)
// ---------------------------------------------------------------------------
/// Derive the base message key for an older message by replaying the chain
/// from the nearest checkpoint.
///
/// This is the "scroll up" scenario the user wants to read a message
/// that was received in the past. Instead of replaying from genesis
/// (which could be millions of steps), we start from the nearest saved
/// checkpoint and replay at most [checkpointInterval] steps.
///
/// **Does not mutate** the main chain keys or counters in [state].
/// May add new checkpoints to [state.checkpoints].
static Future<RatchetAdvanceResult> deriveKeyForOldMessage({
required RatchetState state,
required CCCData cccData,
required int targetCounter,
}) async {
// Find nearest checkpoint at or before targetCounter
int checkpointCounter = 0;
List<int> chainKey = List<int>.from(state.checkpoints[0] ?? state.chainKey);
final sortedKeys = state.checkpoints.keys.toList()..sort();
for (final cp in sortedKeys) {
if (cp <= targetCounter) {
checkpointCounter = cp;
chainKey = List<int>.from(state.checkpoints[cp]!);
} else {
break;
}
}
// Replay chain forward from checkpoint to target
int current = checkpointCounter;
while (current < targetCounter) {
if (cccData.symmetricRatchetEveryMessage) {
chainKey = await _kdf(
cccData,
chainKey,
'next|${state.channelUuid}',
);
}
current += 1;
}
// Derive base_message_key at target
final baseMessageKey = await _deriveMessageKey(
cccData,
chainKey,
targetCounter,
state.channelUuid,
);
// Optionally save a new checkpoint
final interval = cccData.checkpointInterval > 0
? cccData.checkpointInterval
: defaultCheckpointInterval;
if (targetCounter % interval == 0 &&
!state.checkpoints.containsKey(targetCounter)) {
state.checkpoints[targetCounter] = List<int>.from(chainKey);
}
return RatchetAdvanceResult(
baseMessageKey: baseMessageKey,
counter: targetCounter,
);
}
// ---------------------------------------------------------------------------
// Asymmetric Ratchet (KEM) Phase 3 X25519
// ---------------------------------------------------------------------------
/// Perform an asymmetric ratchet step on the **sending** chain.
///
/// Generates a fresh X25519 keypair and mixes the public key bytes into
/// the chain key via KDF. The public key is returned so the caller can
/// embed it in the encrypted payload (``rk`` field). All receivers
/// extract those same bytes and call [applyRemoteAsymmetricRatchet] to
/// perform the identical chain reset.
///
/// ```text
/// freshKeyPair = X25519.newKeyPair()
/// publicKeyBytes = freshKeyPair.publicKey.bytes // 32 bytes
///
/// new_chain_key = KDF(
/// old_chain_key || publicKeyBytes,
/// "asymmetric-reset|" + channelUuid,
/// )
/// ```
///
/// The DH private key is persisted in [state] for potential future
/// pairwise DH key agreement (Phase 4 enhancement for 1-to-1 channels).
///
/// Returns the 32-byte X25519 public key that must be embedded in the
/// message payload.
static Future<List<int>> performAsymmetricRatchet({
required RatchetState state,
required CCCData cccData,
}) async {
// 1. Generate fresh X25519 keypair (32 bytes, OS-level CSPRNG)
final x25519 = X25519();
final keyPair = await x25519.newKeyPair();
final publicKey = await keyPair.extractPublicKey();
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
final publicKeyBytes = publicKey.bytes;
// 2. Mix public-key entropy into the chain
// Using the public key ensures all receivers (group-compatible) can
// compute the same new chain key from information in the payload.
state.chainKey = await _kdf(
cccData,
[...state.chainKey, ...publicKeyBytes],
'asymmetric-reset|${state.channelUuid}',
);
// 3. Persist DH keys for future pairwise DH (Phase 4)
state.dhPrivateKey = privateKeyBytes;
state.dhPublicKey = List<int>.from(publicKeyBytes);
state.lastAsymmetricRatchetCounter = state.lastSentCounter;
return List<int>.from(publicKeyBytes);
}
/// Apply a remote asymmetric ratchet step on the **receiving** chain.
///
/// Called by the message processor after successful decryption when
/// the payload contains an ``rk`` (ratchet-key) field. Applies the
/// same chain-key mixing as [performAsymmetricRatchet] so sender and
/// receiver chains stay in sync.
///
/// [remotePublicKeyBytes] is the 32-byte X25519 public key extracted
/// from the decrypted payload.
static Future<void> applyRemoteAsymmetricRatchet({
required RatchetState state,
required CCCData cccData,
required List<int> remotePublicKeyBytes,
}) async {
// Mix the same public-key entropy that the sender used
state.chainKey = await _kdf(
cccData,
[...state.chainKey, ...remotePublicKeyBytes],
'asymmetric-reset|${state.channelUuid}',
);
// Store remote public key for future DH (Phase 4)
state.remoteDhPublicKey = List<int>.from(remotePublicKeyBytes);
state.lastAsymmetricRatchetCounter = state.lastReceivedCounter;
}
/// Check whether an asymmetric ratchet step is due at the given counter.
static bool _isAsymmetricRatchetDue(int counter, int ratchetFrequency) {
if (ratchetFrequency <= 0) return false; // disabled or every-message (handled separately)
return counter > 0 && counter % ratchetFrequency == 0;
}
// ---------------------------------------------------------------------------
// Key Derivation Functions (Internal)
// ---------------------------------------------------------------------------
/// Core KDF: derive 32-byte key material from input key + info string.
///
/// Uses the KDF function selected in [CCCData.kdfFunction]
/// (SHA-256/384/512/BLAKE2b-512). Output is always truncated to 32 bytes.
static Future<List<int>> _kdf(
CCCData cccData,
List<int> inputKey,
String info,
) async {
final kdfFunction = cccKdfFunctionFromInt(
normalizeCccKdfFunctionValue(cccData.kdfFunction),
);
final hashInput = [...inputKey, ...utf8.encode(info)];
final fullHash = await _hashBytes(hashInput, kdfFunction);
return fullHash.sublist(0, 32); // truncate to 32 bytes
}
/// Derive per-message encryption key from chain key + counter + channel ID.
///
/// Formula: ``KDF(chainKey, counter_bytes(8) || channelUuid)``
///
/// The counter is encoded as 8-byte big-endian to ensure deterministic
/// byte representation across platforms.
static Future<List<int>> _deriveMessageKey(
CCCData cccData,
List<int> chainKey,
int counter,
String channelUuid,
) async {
final counterBytes = _intToBytes8(counter);
final info = '$channelUuid|msg|${String.fromCharCodes(counterBytes)}';
return _kdf(cccData, chainKey, info);
}
/// Hash bytes using the specified KDF/hash function.
static Future<List<int>> _hashBytes(
List<int> bytes,
CccKdfFunction kdfFunction,
) async {
switch (kdfFunction) {
case CccKdfFunction.sha256:
return (await Sha256().hash(bytes)).bytes;
case CccKdfFunction.sha384:
return (await Sha384().hash(bytes)).bytes;
case CccKdfFunction.sha512:
return (await Sha512().hash(bytes)).bytes;
case CccKdfFunction.blake2b512:
return (await Blake2b().hash(bytes)).bytes;
}
}
/// Convert an integer to 8-byte big-endian representation.
static List<int> _intToBytes8(int value) {
return [
(value >> 56) & 0xFF,
(value >> 48) & 0xFF,
(value >> 40) & 0xFF,
(value >> 32) & 0xFF,
(value >> 24) & 0xFF,
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF,
];
}
// ---------------------------------------------------------------------------
// Checkpoint Helpers
// ---------------------------------------------------------------------------
/// Save a checkpoint if the counter is at a checkpoint boundary.
static void _maybeCheckpoint(
Map<int, List<int>> checkpoints,
int counter,
List<int> chainKey,
int checkpointInterval,
) {
final interval =
checkpointInterval > 0 ? checkpointInterval : defaultCheckpointInterval;
if (counter % interval == 0) {
checkpoints[counter] = List<int>.from(chainKey);
}
}
}

View File

@ -0,0 +1,154 @@
///
/// Ratchet State Manager Hive Persistence for Ratchet Chain State
///
/// Manages the lifecycle of [RatchetState] objects in a dedicated Hive box.
/// Each entry is keyed by ``'{channelUuid}_{senderIdentifier}'``.
///
/// Phase 2 of the Hydra Apocalypse Ratchet.
///
import 'package:hive_ce/hive.dart';
import 'package:letusmsg/data/ratchet_state_data.dart';
import 'package:letusmsg/utilz/Llog.dart';
/// Manages persistence of [RatchetState] objects in Hive.
///
/// Provides CRUD operations for ratchet states and channel-level cleanup.
/// The box is opened lazily on first access and remains open for the app
/// lifetime.
///
/// ## Key Format
/// ``'{channelUuid}_{senderIdentifier}'`` deterministic from the
/// [RatchetState.hiveKey] property.
///
/// ## Usage
/// ```dart
/// final manager = RatchetStateManager();
/// await manager.open();
///
/// // Save after ratchet advance
/// await manager.save(state);
///
/// // Load for decrypt
/// final state = await manager.load('chan-uuid', 'sender-id');
/// ```
class RatchetStateManager {
static const String boxName = 'ratchet_state';
Box<RatchetState>? _box;
/// Whether the Hive box is currently open.
bool get isOpen => _box?.isOpen ?? false;
/// Open the ratchet state Hive box.
///
/// Safe to call multiple times returns immediately if already open.
Future<void> open() async {
if (_box != null && _box!.isOpen) return;
try {
_box = await Hive.openBox<RatchetState>(boxName);
} catch (ex) {
Llog.log.e('[RatchetStateManager] Failed to open box: $ex');
rethrow;
}
}
/// Ensure the box is open, opening it if necessary.
Future<Box<RatchetState>> _ensureBox() async {
if (_box == null || !_box!.isOpen) {
await open();
}
return _box!;
}
/// Load ratchet state for a specific channel + sender.
///
/// Returns ``null`` if no state exists (channel not yet initialized with
/// ratchet, or pre-Phase 2 channel).
Future<RatchetState?> load(
String channelUuid,
String senderIdentifier,
) async {
final box = await _ensureBox();
final key = _buildKey(channelUuid, senderIdentifier);
return box.get(key);
}
/// Save ratchet state after advancing the chain.
///
/// Must be called after every send/receive operation to persist the
/// updated chain keys and counters.
Future<void> save(RatchetState state) async {
final box = await _ensureBox();
await box.put(state.hiveKey, state);
}
/// Delete ratchet state for a specific channel + sender.
Future<void> delete(
String channelUuid,
String senderIdentifier,
) async {
final box = await _ensureBox();
final key = _buildKey(channelUuid, senderIdentifier);
await box.delete(key);
}
/// Delete all ratchet states for a channel (all senders).
///
/// Called when a channel is deleted or left. Iterates all keys in the box
/// and removes those matching the channel prefix.
Future<void> deleteChannel(String channelUuid) async {
final box = await _ensureBox();
final prefix = '${channelUuid}_';
final keysToDelete = <dynamic>[];
for (final key in box.keys) {
if (key.toString().startsWith(prefix)) {
keysToDelete.add(key);
}
}
if (keysToDelete.isNotEmpty) {
await box.deleteAll(keysToDelete);
Llog.log.d('[RatchetStateManager] Deleted ${keysToDelete.length} '
'ratchet state(s) for channel $channelUuid');
}
}
/// List all ratchet states for a channel (all senders).
///
/// Useful for debugging and testing.
Future<List<RatchetState>> listForChannel(String channelUuid) async {
final box = await _ensureBox();
final prefix = '${channelUuid}_';
final results = <RatchetState>[];
for (final key in box.keys) {
if (key.toString().startsWith(prefix)) {
final state = box.get(key);
if (state != null) results.add(state);
}
}
return results;
}
/// Check if a ratchet state exists for a channel + sender.
Future<bool> exists(
String channelUuid,
String senderIdentifier,
) async {
final box = await _ensureBox();
return box.containsKey(_buildKey(channelUuid, senderIdentifier));
}
/// Close the Hive box.
///
/// Called during app shutdown or cleanup.
Future<void> close() async {
if (_box != null && _box!.isOpen) {
await _box!.close();
}
_box = null;
}
/// Build the composite Hive key from channel + sender identifiers.
static String _buildKey(String channelUuid, String senderIdentifier) {
return '${channelUuid}_$senderIdentifier';
}
}