Security Policy
Cohrtz is designed with a defense-in-depth security model that ensures data privacy, integrity, and authenticity through end-to-end encryption (E2EE) and decentralized trust. The server never has access to your plaintext data or encryption keys—only encrypted blobs transit the network.
Last updated: February 19, 2026
Security Architecture Overview
The security model is built on five primary pillars:
| Pillar | Description |
|---|---|
| Local Identity & Hardware-Backed Keys | Cryptographic seeds stored in OS-level secure enclaves (Keychain/Keystore) |
| Peer-to-Peer Trust | Ed25519 digital signatures for packet authentication |
| Pairwise Key Exchange | X25519 Diffie-Hellman for deriving shared secrets |
| Group Key Management | TreeKEM (RFC 9420-inspired) for forward secrecy and efficient group rekeying |
| Secure Synchronization | Vector clock-based CRDT sync with encrypted payloads and Merkle root consistency checks |
1. Local Security & Identity
Upon first launch, the app creates a unique user identity and generates cryptographic key material that never leaves the device in plaintext form.
Key Generation
| Algorithm | Purpose | Key Size |
|---|---|---|
| Ed25519 | Digital signatures | 256-bit |
| X25519 | Key exchange | 256-bit |
| AES-GCM-256 | Symmetric encryption | 256-bit |
| Argon2id | KDF (native only) | 256-bit |
Hardware-Backed Storage
| Platform | Storage |
|---|---|
| iOS | Keychain (SEP) |
| macOS | Keychain (T2/M-series) |
| Android | Keystore (TEE/StrongBox) |
| Web | SharedPreferences (fallback, development only) |
Local Security Initialization Flow
SharedPreferences?} Load_Profile -- No --> Create_ID[Create UUID v7 User ID] Load_Profile -- Yes --> Parse_ID[Parse UserProfile JSON] Create_ID --> Save_ID[Save Profile to SharedPreferences] Parse_ID --> Sec_Init[SecurityService.initialize] Save_ID --> Sec_Init Sec_Init --> Read_Seeds[Read Seeds from SecureStorageService] Read_Seeds --> Seeds_Exist{Seeds in
Keychain/Keystore?} Seeds_Exist -- No --> Gen_Ed[Generate Ed25519 KeyPair] Gen_Ed --> Gen_X[Generate X25519 KeyPair] Gen_X --> Save_Seeds[Save Private Seeds to Secure Storage] Seeds_Exist -- Yes --> Derive_Ed[Derive Ed25519 KeyPair from Seed] Derive_Ed --> Derive_X[Derive X25519 KeyPair from Seed] Save_Seeds --> Ready((Service Ready)) Derive_X --> Ready
2. Peer-to-Peer Trust & Handshake
Before any data is synchronized, peers must exchange public keys to establish cryptographic trust. This handshake forms the foundation of the E2EE protocol.
Handshake Mechanism
-
1.
Key Broadcast: Upon connecting to a room, each peer broadcasts a
HANDSHAKEpacket containing Ed25519 signing public key (32 bytes), X25519 encryption public key (32 bytes), and optional TreeKEM public key. - 2. Key Storage: HandshakeHandler maintains maps for signing keys and encryption keys indexed by sender ID.
- 3. Packet Buffering: Packets from unknown peers are queued until their handshake is received and validated.
Packet Signature Verification
Every packet includes an Ed25519 signature over:
-
type- Packet type enum -
request_id- UUID for correlation -
sender_id- Participant identity -
payload- Encrypted content -
chunk_index- For large payloads -
is_last_chunk- Stream flag -
target_id- Unicast destination -
encrypted- Encryption flag
Peer Trust Sequence
[Ed25519 PubKey, X25519 PubKey] B->>Room: Receive Handshake Note over B: HandshakeHandler stores
User A's public keys B->>A: Reply HANDSHAKE
[Ed25519 PubKey, X25519 PubKey] Note over A: HandshakeHandler stores
User B's public keys rect rgb(200, 230, 200) Note over A,B: Trust Established
All subsequent packets verified via Ed25519 signature end A->>B: SYNC_REQ [Signed + GSK-encrypted when available] Note over B: SecurityService.verifyPacket()
validates signature against stored key
3. Pairwise Encryption
For unicast communication (e.g., GSK sharing, secure sync responses), Cohrtz uses X25519 Diffie-Hellman to derive shared secrets.
Shared Secret Derivation
- 1. Perform X25519 key agreement between local private key and remote public key
-
2.
Derive symmetric key using SHA-256:
Hash(sharedSecret || salt) - 3. Use derived 256-bit key for AES-GCM-256 encryption
AES-GCM-256 Payload Format
+--------+------------+-----+
| Nonce | Ciphertext | MAC |
| 12 B | Variable | 16 B|
+--------+------------+-----+
The 12-byte nonce ensures uniqueness, while the 16-byte MAC provides authenticity verification.
4. Group Key Management (TreeKEM)
For efficient and secure group communication, Cohrtz implements TreeKEM based on RFC 9420 (Messaging Layer Security).
GSK
256-bit AES-GCM key shared by all group members
Ratchet Tree
Binary tree structure where each node holds key material
Forward Secrecy
Keys rotate on member join/leave
O(log n)
Logarithmic complexity for member updates
Group Key Lifecycle
with single leaf] end subgraph "Member Join" Join[New Member Joins] --> Welcome[TreekemService.welcomeNewMember] Welcome --> |HPKE-sealed path secret| NewMember[New member derives GSK] Welcome --> Rotate1[Host calls createUpdate] Rotate1 --> FwdSec1[Rotate GSK
Forward Secrecy] end subgraph "Member Leave" Leave[Member Leaves] --> Blank[Blank leaf + direct path] Blank --> Rotate2[Remaining member calls createUpdate] Rotate2 --> FwdSec2[Rotate GSK
Post-Compromise Security] end FwdSec1 --> SaveState[Save TreeKEM state
Public → CRDT
Private → SecureStorage] FwdSec2 --> SaveState
5. Secure Data Synchronization
The synchronization protocol ensures CRDT data remains consistent across all nodes without exposing plaintext to the network.
SYNC_REQ / SYNC_CLAIM Transport
- Primary path:
SYNC_REQandSYNC_CLAIMare GSK-encrypted broadcasts when the Group Secret Key is available. - Fallback: If the GSK is not yet available (for example first join), the protocol falls back to pairwise unicast to known peers.
- Data response: The actual changeset is always pairwise encrypted and sent directly to the requester.
Vector Clock Synchronization
Each CRDT database maintains a Vector Clock tracking the latest Hybrid Logical Clock (HLC) timestamp from each peer:
{
"user:01234...": "2024-01-15T10:30:00.000Z-0001-abcd1234",
"user:56789...": "2024-01-15T10:30:01.500Z-0002-efgh5678"
}
Consistency Verification
Peers periodically broadcast CONSISTENCY_CHECK containing:
-
record_count- Total records across all tables -
merkle_root- SHA-256 hash of sorted record HLCs -
table_counts- Per-table record counts
Synchronization Protocol Sequence
Both start jitter timers end B-->>A: [Timer: 120ms] C-->>A: [Timer: 145ms] Note over B: Timer expires first B->>A: SYNC_CLAIM B->>C: SYNC_CLAIM Note over C: Cancel timer
(lost election) Note over B: CrdtService.getChangesetFromVector
Generate delta changeset B->>A: DATA_CHUNK [AES-GCM Encrypted, Ed25519 Signed] Note over A: 1. Verify Ed25519 signature
2. Derive shared secret (X25519)
3. Decrypt with AES-GCM-256
4. Merge into local SQLite CRDT A->>B: CONSISTENCY_CHECK [Merkle Root + Diagnostics] A->>C: CONSISTENCY_CHECK [Merkle Root + Diagnostics]
6. Invite System Security
Group invites use a secure code-based system with optional single-use tokens.
Invite Flow
- 1.Host generates invite code (alphanumeric, configurable expiration)
- 2.Invite metadata stored in CRDT (
group_settingstable) - 3.Invitee connects to public invite room using group name
- 4.Invitee broadcasts
INVITE_REQwith code - 5.Host validates code against stored invites
- 6.On success:
INVITE_ACKwith private data room UUID - 7.On failure:
INVITE_NACKwith rejection reason
Invite Security Properties
| Property | Implementation |
|---|---|
| Rate Limiting | Host controls invite generation |
| Expiration | Configurable expiresAt timestamp |
| Single-Use | Optional isSingleUse flag, consumed after successful join |
| Room Separation | Invite negotiation on public room; data sync on private UUID room |
Packet Types Reference
| Type | Value | Description | Encryption |
|---|---|---|---|
SYNC_REQ | 0 | Request missing CRDT data with vector clock | Signed |
SYNC_CLAIM | 1 | Election winner acknowledgment | Signed |
DATA_CHUNK | 2 | Encrypted CRDT changeset payload | GSK or Pairwise |
HANDSHAKE | 3 | Public key exchange | Signed |
CONSISTENCY_CHECK | 4 | Merkle root + diagnostics | Signed |
INVITE_REQ | 5 | Request to join via invite code | Signed |
INVITE_ACK | 6 | Invite accepted, includes data room UUID | Signed |
INVITE_NACK | 7 | Invite rejected | Signed |
UNICAST_REQ | 8 | Initiate direct communication | Pairwise |
UNICAST_ACK | 9 | Confirm unicast channel | Pairwise |
Cryptographic Standards Summary
| Category | Algorithm | Standard | Notes |
|---|---|---|---|
| Symmetric Encryption | AES-GCM-256 |
NIST SP 800-38D | 12-byte nonce, 16-byte MAC |
| Digital Signatures | Ed25519 |
RFC 8032 | 256-bit keys, deterministic |
| Key Exchange | X25519 |
RFC 7748 | Curve25519 ECDH |
| Key Derivation | Argon2id |
RFC 9106 | Memory-hard, side-channel resistant |
| Hash Function | SHA-256 |
FIPS 180-4 | Used for HKDF, Merkle roots |
| HKDF | HKDF-SHA256 |
RFC 5869 | Extract-and-Expand |
| Group Key Management | TreeKEM |
RFC 9420 (MLS) | Ratchet tree structure |
| Timestamps | Hybrid Logical Clock |
Lamport + Physical | Causality-preserving |
Threat Model & Mitigations
| Threat | Mitigation |
|---|---|
| Server Compromise | Server only sees encrypted blobs; cannot decrypt without GSK |
| Man-in-the-Middle | Ed25519 signatures authenticate all packets; X25519 provides key agreement |
| Key Compromise | TreeKEM forward secrecy limits exposure; key rotation on member changes |
| Replay Attacks | Request IDs (UUID v4) + HLC timestamps prevent replays |
| Malicious Member | Post-compromise security via TreeKEM rotation on leave |
| Brute Force (Keys) | 256-bit keys provide 128-bit security level |
| Brute Force (KDF) | Argon2id memory-hard parameters resist GPU/ASIC attacks |
| Side Channels | Ed25519/X25519 have constant-time implementations |
Reporting Security Issues
If you discover a security vulnerability, please email security@cohrtz.com with:
Description of the vulnerability
Detailed explanation of the issue
Steps to reproduce
Clear reproduction instructions
Potential impact assessment
Severity and scope of the issue
Any suggested mitigations
Potential fixes or workarounds
We aim to respond within 48 hours and will coordinate disclosure timelines with you.
Questions about our security?
We're committed to transparency and would be happy to discuss our approach.
Back to Home