NEW: init commit
This commit is contained in:
commit
df1de9d99d
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 16–24 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 — ~1–2 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 member’s 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 30–80 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=48–64, length=16–24)
|
||||
3. Inner AEAD encrypt plaintext → inner_ct (triple AEAD with base_message_key).
|
||||
4. Cascade encrypt inner_ct using the 16–24 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 16–24 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 + 32–64 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.
|
||||
|
|
@ -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 recipient’s 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).
|
||||
|
|
@ -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 (32–64 bytes of true randomness).
|
||||
|
||||
5. Both sides now take this fresh shared secret and mix it into the symmetric chain:
|
||||
|
||||
```text
|
||||
new_chain_key = KDF(old_chain_key, fresh_shared_secret, "asymmetric-reset" + groupId)
|
||||
```
|
||||
|
||||
6. The old symmetric chain key is deleted forever.
|
||||
|
||||
This is the “reset”.
|
||||
It injects **brand new, high-entropy randomness** that the attacker does not have, even if they had stolen all previous keys.
|
||||
After this step, the conversation “heals” — past compromises no longer matter for future messages.
|
||||
|
||||
This is exactly how post-compromise security works.
|
||||
|
||||
|
||||
|
||||
3. What Exactly Is Stored on Your Device (Very Specific)
|
||||
========================================================
|
||||
|
||||
For each chat (channel) we store exactly these things:
|
||||
|
||||
- **CCCData** – one object per channel containing every setting you chose (which providers, which KEM, which AEADs, ratchet frequency, padding size, etc.).
|
||||
|
||||
- **Raw encrypted wire blobs** – for every message that ever arrived, we store the **exact bytes** that came from the relay. These are the permanent chat history. They are stored in Hive exactly as received.
|
||||
|
||||
- **Ratchet state** (one set per person you are chatting with in that channel):
|
||||
- encryptedRootKey (the original shared secret, encrypted under your device master key)
|
||||
- currentChainKey (the latest symmetric chain key, 32 bytes)
|
||||
- lastProcessedCounter (the highest message counter we have successfully decrypted for this person)
|
||||
- checkpoints (a small map: every 500 messages we save a snapshot of the chain key at that counter so we don’t have to replay 10,000 steps when loading very old messages)
|
||||
|
||||
We **never** store a key for each individual message.
|
||||
We only keep the current state of the ratchet plus the raw encrypted blobs.
|
||||
|
||||
|
||||
|
||||
4. Message Flow From One User to Another (Detailed)
|
||||
===================================================
|
||||
|
||||
**Sending Side (User A → User B)**
|
||||
|
||||
1. User A types a message.
|
||||
2. App loads CCCData for this channel (all your chosen algorithms).
|
||||
3. App loads User A’s sending ratchet state for this channel.
|
||||
4. App advances the symmetric chain (runs KDF on currentChainKey to produce newChainKey).
|
||||
5. App derives base_message_key = KDF(newChainKey, currentCounter + 1, groupId).
|
||||
6. App pads the plaintext using the block size and random padding bytes you set in CCCData.
|
||||
7. App encrypts the padded plaintext using the list of AEAD algorithms in CCCData (round-robin).
|
||||
8. App puts the ciphertext into RelayMessage.data.
|
||||
9. App sends the RelayMessage to the relay.
|
||||
10. App updates its sending ratchet state (saves newChainKey and increments counter) and stores the raw RelayMessage locally.
|
||||
|
||||
**Receiving Side (User B receives)**
|
||||
|
||||
1. User B receives RelayMessage from relay.
|
||||
2. App immediately stores the exact raw bytes in Hive.
|
||||
3. App loads CCCData and User A’s receiving ratchet state.
|
||||
4. App advances the receiving chain forward until it reaches the incoming message counter.
|
||||
5. App derives the same base_message_key.
|
||||
6. App decrypts using the same AEAD list from CCCData.
|
||||
7. App removes padding and shows the message.
|
||||
8. App updates and saves the receiving ratchet state.
|
||||
|
||||
|
||||
|
||||
5. How We Load Messages on Cold Start
|
||||
=====================================
|
||||
|
||||
**Recent Messages (what you see immediately)**
|
||||
|
||||
1. You open the app and authenticate → device master key is created.
|
||||
2. App loads CCCData and the current ratchet state for each person.
|
||||
3. App loads the newest raw blobs from storage.
|
||||
4. For each new message, it advances the ratchet forward from the last saved state, derives the base message key, decrypts, and shows it.
|
||||
5. After decrypting, it updates the saved ratchet state.
|
||||
|
||||
**Older Messages (when you scroll up)**
|
||||
|
||||
1. You scroll up.
|
||||
2. Older messages appear grayed out with “Tap to unlock older messages”.
|
||||
3. When you tap, the app asks for your passphrase or biometrics again.
|
||||
4. If successful, the app replays the ratchet forward from the last saved state to the old messages you want to see.
|
||||
5. It derives the correct base message key for each old message, decrypts the raw blob, and shows it.
|
||||
6. It updates the ratchet state and checkpoints so next time it is faster.
|
||||
|
||||
|
||||
|
||||
6. How the Ratchet Chain Is Started (Channel Creation / Invite)
|
||||
===============================================================
|
||||
|
||||
1. Creator generates a fresh 32-byte root secret.
|
||||
2. Root secret is encrypted under the recipient’s public key (using the KEM you chose in CCCData) and sent in the invite.
|
||||
3. Recipient decrypts the root secret.
|
||||
4. Both sides run the same initialization:
|
||||
- sendingChainKey = KDF(rootSecret, "sending" + groupId)
|
||||
- receivingChainKey = KDF(rootSecret, "receiving" + groupId)
|
||||
5. Both save the encrypted root and initial chain keys.
|
||||
6. Counters start at 0.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 16–24 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 (16–32 bytes).
|
||||
- Ultra mode: pad to 64-byte boundary + 32–64 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 (0–9) in ``ChannelCccProfile`` mapping to different
|
||||
provider/cipher combinations. Combos 1–4 are multi-provider scaffolds;
|
||||
combos 5–9 are user-selectable single/dual/triple AEAD configurations.
|
||||
- Iteration expansion (1–16x 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 (0–9), 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 5–9 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 (8–13):
|
||||
|
||||
- ``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 8–11):
|
||||
``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`` (16–24, default 20).
|
||||
- ``cascadePoolSize`` (48–64, 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 20–50 messages.
|
||||
|
||||
- **Layer B – Massive Secret Cascade**:
|
||||
|
||||
- Build cascade pool: 48–64 unique ``(provider, algorithm)`` pairs from
|
||||
all ``enabledProviders`` across symmetric, AEAD, PQ-KEM, PQ-signature, hash categories.
|
||||
- Per-group secret permutation: select 16–24 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 (16–24 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`` (0–100): performance/cost ranking (``efficient`` mode primary input).
|
||||
- ``reliabilityScore`` (0–100): 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 20–24 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.
|
||||
|
|
@ -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 16–24 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; // 16–24 (default 20)
|
||||
@HiveField(21) final int cascadePoolSize; // 48–64 (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 48–64 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 (16–24 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 16–24 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).
|
||||
|
|
@ -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 **16–24 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 16–24 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 16–24.
|
||||
- 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 (16–24 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 recipient’s 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.
|
||||
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)}%'
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)';
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue