Core51 Encryption & Security Overview ==================================== Last updated: 2026-04-22 This document explains how Core51 protects confidentiality and integrity for messages and attachments. It is written as a technical but readable overview for engineers, auditors, and security reviews. Important framing ----------------- Core51 is designed as an end‑to‑end encrypted business messenger. - Message content is encrypted on the client device. - The server is treated as untrusted with respect to plaintext: it stores ciphertext and metadata. - Only chat participants can decrypt. This overview focuses on: - key material - message encryption and integrity - message delivery metadata - attachments (R2/S3) encryption and verification - exports and verifier expectations It intentionally does not list HTTP endpoints. 1) Key material and identity ---------------------------- Each user has two long‑term keypairs: 1) Signing key (Ed25519) - Purpose: authenticity and integrity of message payloads. - Used to sign canonical message payloads. - Public signing key is published to the server. 2) Encryption key (X25519 / Curve25519) - Purpose: wrapping (encrypting) message keys and file keys for each participant. - Public encryption key is published to the server. Key continuity / TOFU --------------------- Core51 uses a TOFU‑style (Trust‑On‑First‑Use) model: - The first observed public keys for a contact are pinned. - If a contact's keys change, the client UI can warn and may block outgoing messages until the user explicitly re‑trusts. Key rotation ------------ Users may rotate keys. Rotation affects offline verification: - Old signatures may not validate against the *current* public signing key. - For stronger evidence, Core51 can store/export the sender public signing key that was used at send time. 2) Message cryptography (v2) ---------------------------- 2.1 Payload encryption Core51 v2 uses a hybrid model: - The message plaintext is encrypted with a fresh random symmetric key (msg_key). - Symmetric algorithm: AES‑256‑GCM. - Nonce: 12 bytes random. - AAD for message bodies: empty. Transported/stored ciphertext includes the GCM authentication tag (ciphertext||tag). 2.2 Key envelopes (per‑recipient wrapping) The symmetric msg_key is never sent in plaintext. Instead it is wrapped separately for: - the recipient - the sender (encrypt‑to‑self) This is done via "envelopes". Envelope algorithm (Android‑compatible) -------------------------------------- Core51 currently uses a NaCl box construction for message envelopes: - wrap_algo: crypto_box_v1 - nonce: 24 bytes random - wrapped_key: crypto_box(msg_key, nonce24, sender_enc_secret_key, recipient_enc_public_key) To decrypt a message: - the recipient selects the envelope addressed to their user_id - opens the NaCl box using: - sender encryption public key - recipient encryption secret key - obtains msg_key (32 bytes) - decrypts message ciphertext using AES‑GCM 2.3 Ciphertext integrity binding For every message, the ciphertext is hashed: - ciphertext_hash = SHA‑256(ciphertext_bytes) The server and clients can verify: - sha256(decoded ciphertext) == ciphertext_hash This prevents trivial corruption/substitution of ciphertext bytes. 2.4 Canonical payload and signatures The sender signs a canonical JSON payload (Ed25519). Canonicalization rules: - UTF‑8 - sorted keys - no whitespace The signed payload binds: - msg_id - conversation_id - sender_id / recipient_id - created_at - gcm_nonce - ciphertext_hash - envelopes_hash - prev link (see chain) - optional reply_to_msg_id The signature provides: - authenticity: who sent it - integrity: which exact fields are covered 2.5 Hash chain Messages form a per‑conversation hash chain: - payload_hash = SHA‑256(canonical_signed_payload_bytes) - each message includes a reference to a previous hash. This allows a verifier to detect missing/reordered messages *inside the exported set*. Server vs client chain ---------------------- Depending on client behavior, the value the client signs as "prev" may be null or may reference a previous message. The server may also maintain its own canonical chain value. For strong offline evidence, Core51 can store/export: - the server‑verified payload hash - the exact prev value that was part of the signature 3) Server responsibilities and threat model ------------------------------------------- The server is untrusted for plaintext, but trusted for: - access control decisions - storing ciphertext and per‑recipient envelopes - enforcing membership rules (only chat participants may send/read) - exposing exports with integrity metadata Access control -------------- Server enforces that: - only chat members can list/export a chat - only chat members can access attachment metadata - only chat members can obtain presigned download URLs for attachments This prevents a user from downloading other users' chats/attachments unless there is an access control bug. 4) Attachments (documents & images) ----------------------------------- 4.1 Storage model Attachments are stored as ciphertext objects in Cloudflare R2 (S3). - The server does not store plaintext files. - The server issues presigned URLs for upload/download. 4.2 Encryption A file is encrypted locally before upload: - fresh random file_key (32 bytes) - AES‑256‑GCM - nonce 12 bytes random - AAD is a JSON string (aad_json) that must match on decrypt The resulting ciphertext is uploaded to R2. 4.3 Ciphertext verification For every attachment, store: - ciphertext_sha256 = SHA‑256(ciphertext_bytes) When downloading an attachment, the client/verifier must: - download ciphertext - verify sha256(ciphertext) matches ciphertext_sha256 - only then attempt decryption This prevents substitution of the stored object. 4.4 Wrapping the file key for recipients The file_key is wrapped per recipient using NaCl box, in a mapping: - wrapped_keys_json: { user_id: base64(nonce24 || box_cipher) } To decrypt: - select your user_id entry - parse packed = nonce24 || box_cipher - open NaCl box using sender enc public key + your enc secret key - obtain file_key - AES‑GCM decrypt ciphertext with nonce12 and AAD 4.5 Attachment messages A file is referenced from within a message as a small JSON object in the message plaintext: - kind = "attachment" - attachment_id - filename - mime - size - optional caption Metadata is currently not encrypted at rest on the server. The file contents are encrypted end‑to‑end. 5) Exports and offline verification ----------------------------------- Core51 supports two export levels: - v1 (legacy): message rows without v2 envelopes; integrity checks only; decryption not supported. - v2 (recommended): includes message envelopes and stronger audit fields. Offline verifier checks: - ZIP integrity: sha256 of files matches manifest - JSONL validity - hash chain consistency - signatures (best‑effort; strict when required fields are present) - optional decryption using the user's encryption private key Why some messages may remain undecryptable ----------------------------------------- A message can be undecryptable if the server does not have the envelope for the recipient. Core51 v2 requires envelopes to be stored; older data may lack them. 6) Operational security notes ----------------------------- - Never share private keys casually. Use a controlled process when plaintext must be revealed. - For verifier tests, keys/tokens may be provided via CLI, but this is not recommended for production. - Prefer providing secrets via stdin or environment variables. 7) Interpretation of verification results ----------------------------------------- - PASS: checks succeeded. - WARN: evidence is internally consistent, but strict verification is not possible for some items. Common causes: - legacy messages missing audit fields - key rotation (no historical key available) - FAIL: integrity is broken (hash mismatch, chain mismatch, structural corruption).