review this for cryptographic safety. Anything ambigous should be assumed to follow industry standard protocol. When giving a review use the langauge and termenology used in the security document when the differ or conflict with industry standard terms.
- Mostly full post compormise security after a time in which the DH chain is updated. An attacker should not have access to more than one chain for each client.
- Full forward security, no messages from before the epoch should be decryptable.
- Chain (a group of messages sent by the same client one after another. Bookended by messages from the other client - The messages from the other client are not included- )
- DH chain (Diffe-Hellman Chain. The larger of the two rachetts. Cycles RK)
- CK chain (Chain Key Chain. The smaller of the two rachetts. Cycles CK)
- DM (Direct Message)
- Leader (The person who initiates a DM)
- Receiver (The other member of the DM that did not create it. It is NOT the person who receives an individual message)
- Epoch (moment in time in which an event happens)
- IK (Perminant Identity Keys)
- OPK (One time pre-key)
- SPK (Semi-Perminant signed pre key)
- EK (Ephemeral Key)
- RK_n (Root Key. n symbolizes the iternation that n is on)
- CK (Chain key, can be suffixed with: s(for send), r(for recieving), L(for Leader), R(for Receiver), n (an intiger for the iteration of that key))
- publicKey_n (The public key used for the n interation of the DH chain)
- privateKey_n (The private key used for the n interation of the DH chain)
- n (In most contexts it means the index of the recieved message in the CK chain. Only can be given directly from the header of a received message. Not called n when stored)
- pn (Reported number of messages sent in the CK chain prior. Reported by the header of a recievd message, and only can be given directly from that header. Not called pn when stored)
- nrs (The stored value of the projected index of the next message to be recieved. Should be equal to n in most cases when a new message is recieved)
- pnrs (The stored value of the pn as seen by the latest recived message)
- ns or nextN (same term) (The index of the message that is about to be sent. In most cases the next time a client sends a message the n in the header of that message will be equal to ns at time of creation)
- pns (The number of messages the client set in its last set of CK chain messages.)
Inintialization is created using the x3dh protocol using IK, EK , OPK generated by the receiver, and SPK
Generated is RK_0, CK_s, CK_r, all for the leader.
The Recipient then does the same x3dh protocol.
Generated is RK_0, CK_s, CK_r, all for the recipient.
When a DM is created a message is automatically send from the Leader. This is what starts the initialization & x3dh protocol. The messages header contains EK for the Leader in place of pubicKey_n
At the end ns = 1 most other metadata is undefined CK chain is updated to CK_1
The user may continue sending messages just like in any other CK chain.
When the first DM is received initalization and x3dh is run for the reciever.
The user than decrypts the welcome DM and CK_0 becomes CK_1.
Metadata: nrs = 1
There are eight destinct message events not including the welcome message or out of order messages
- Leader sends first message in chain
- Leader sends all other messages in a chain
- Receiver sends first message in chain
- Receiver sends all other messages in a chain
- Leader receives first message in the other user's chain
- Leader receives all other messages in the other user's chain
- Receiver receives first message in the other user's chain
- Receiver receives all other messages in the other user's chain
The easist of the decryptions is solved through a "simple decryption". Messages that can be solved by a simple decryption include
- Receiver receives all other messages in the other user's chain
- Leader receives all other messages in the other user's chain
The easiest of the encryptions is solved through a "simple encryption". Messages that can be solved by a simple encryption include.
- Leader sends first message in chain
- Leader sends all other messages in a chain
- Receiver sends all other messages in a chain
There is one special case where a DH chain cycle does not happen
- Receiver receives first message in the other user's chain
There are two special cases where a DH chain cycle happens
- Receiver sends first message in chain
- Leader receives first message in the other user's chain
Inputs CK_n, ciphertext Ouputs CK_n+1, rawtext
nrs is set to the n of the messsage header + 1 pnrs is set to the pn of the message header publicKey_n from the header is stored for later use
Inputs CK_n, rawtext Outputs CK_n+1, ciphertext
ns is incemented
pns is set to ns ns is set to 0
Proceeds with simple decryption
new privateKey_n and publicKey_n are generated and stored
A dh process happens
- RK_n+1, CK_s_n+1, CK_r_n+1 = KDF(RK_n, DH(privKey_n, publicKey_n_from_other))
- Inputs: other client's publicKey_n from storage, client's privateKey_n, RK_n
- Outputs: RK_n+1, CK_s_n+1, CK_r_n+1
CK_s_n+1 is then used in a simple encryption to encrypt the next message. The ouput from that along with RK_n+1, and CK_r_n+1 are all stored.
ns is then incrimented
new privateKey_n and publicKey_n are generated and stored A dh process happens
- RK_n+1, CK_s_n+1, CK_r_n+1 = KDF(RK_n, DH(privKey_n, publicKey_n_from_other))
- Inputs: other client's publicKey_n from the header, client's privateKey_n, RK_n
- Outputs: RK_n+1, CK_s_n+1, CK_r_n+1
ns is set to 0 pns is set to what ns was before it was set to 0 other client's publicKey_n is stored the ouputs from the dh proccess is stored
the proccess then proceeds with simple decryption to decrypt the message.
This proccess should create a secure double rachett encryption, assuming no messages arrive out of order.
While we recognise the importance of solving out of order messaging, it is also important to make sure the protocol is secuire first. That is why we need a comprehensive plan for the rest of the protocl before moving towards solving out of order messaging.
All keys after they have been used in their reasonable lifecycle. Excess chain keys and root keys are not stored after their use.
Instead up updating the rachett when a new publicKey_n is detected, it instead enforces strict rules opun both parties. When sending the first message after receiving a message the Receiver MUST cycle the DH rachett, additionally when receiving the first messages after sending a message the leader MUST cycle teh DH rachett.
Findings are organized by severity: Critical → High → Medium → Low/Informational.
C-1 — Firestore storage rules fully open
- File:
storage.rules - Every authenticated (and unauthenticated) user can read and write any file stored in Firebase Storage. There is no
request.auth != nullguard, no path-scoped rule, and no file-size limit. - Impact: Exfiltration of all uploaded files; storage-cost abuse; replacement of any file (e.g. profile images) with malicious content.
C-2 — Unauthenticated writes to /messages collection
- File:
firestore.rules, message write rule allow write: if trueon the messages collection (or equivalent path) allows any actor — authenticated or not — to create, modify, or delete messages.- Impact: Message injection, deletion of chat history, spoofing of
sentBymetadata.
C-3 — Cloud Functions addOPK / getOPK have no auth check
- File:
functions/index.js - Neither function checks
context.authbefore processing. Any unauthenticated caller can exhaust a victim's OPK pool (forcing OPK reuse) or inject arbitrary OPK material. - Impact: Breaks forward secrecy of X3DH; enables key-material injection.
C-4 — Unauthenticated file-upload endpoint
- File:
lib/GroupMessageHandler.ts(upload path),storage.rules - File uploads are not gated on session auth at the rules layer. Combined with C-1, any actor can upload files attributed to another user.
- Impact: Storage abuse; content injection into other users' threads.
C-5 — Friend-list / contact-list poisoning via unauthenticated write
- File:
firestore.rules, users/friends sub-collection - Missing
request.auth.uid == userIdguard on friend-list writes. An attacker can add themselves (or remove others) from any user's friend list. - Impact: Social-graph manipulation; bypasses
canMessage()if friend status gates messaging access.
C-6 — Forged UNENCRYPTED_TREE update accepted without sender verification
- File:
lib/GroupMessageHandler.ts,handleMessage/startUpdatepaths UNENCRYPTED_UPDATE/UNENCRYPTED_TREEmessages are processed based solely on theirsentBy.idfield, which is stored in Firestore and is writable (see C-2). Any group member (or external attacker via C-2) can forge a tree-update message and replace the KEM tree for all recipients.- Impact: Full group key compromise; attacker gains decryption access to all future messages.
C-7 — removeUser not authority-checked
- File:
lib/GroupMessageHandler.ts,removeUser/ Firestore rules - Group membership removal is gated only on Firestore client-side logic; the Firestore rule does not verify the caller is the group owner/admin. Any group member can remove any other member.
- Impact: Targeted denial-of-service against group members; forced epoch rotation harassment.
C-8 — HttpsError never imported in Cloud Functions
- File:
functions/index.js new functions.https.HttpsError(...)is referenced for error paths butHttpsErroris not imported. The function crashes with aReferenceErrorinstead of returning a structured error, silently swallowing auth-path rejections and making error handling unreliable.- Impact: Auth rejection paths throw unhandled exceptions, potentially leaking stack traces to clients.
H-1 — DH output used directly as AES key (missing KDF step)
- File:
lib/e2ee/e2ee.js, DH-to-key derivation path - The raw ECDH shared secret is imported directly as an HKDF or AES key without an intermediate KDF step that binds context (session ID, identities, purpose string). This violates standard key-derivation hygiene.
- Impact: If the same DH pair is reused in a different context, the derived key is identical; context confusion attacks become possible.
H-2 — Fixed zero HKDF salt
- File:
lib/e2ee/e2ee.js, HKDF invocations - Several HKDF calls pass a zeroed or empty salt instead of a random or protocol-defined salt. A zero salt causes HKDF-Extract to behave as a pure PRF on the IKM, removing the domain-separation benefit of the salt.
- Impact: Weakened key separation; different protocol steps that share IKM may produce correlated keys.
H-3 — X3DH receiver does not verify the sender's identity-key binding
- File:
lib/e2ee/e2ee.js/lib/MessageHandler.js, X3DH receive path - The receiver performs the DH computations but does not verify that the
IKincluded in the welcome message matches the sender's published identity key. An attacker who can write to Firestore (see C-2) can substitute their own IK in the welcome message. - Impact: Identity misbinding; attacker impersonates a user at the X3DH layer.
H-4 — canRequest / canMessage checks rely on client-supplied fields
- File:
firestore.rules,canRequest()/canMessage()functions - Field names referenced inside
canRequest()do not match the actual document schema (wrong field names), so the guard always evaluates to a default permissive branch. - Impact: Intended access controls are silently bypassed; any authenticated user can initiate DMs regardless of friend/block status.
H-5 — Predictable, sequential OPK IDs
- File:
functions/index.js, OPK generation/storage - OPK records are stored with sequential numeric IDs. An attacker who can enumerate Firestore documents can harvest all pre-keys before they are consumed, breaking X3DH forward secrecy.
- Impact: Offline pre-key exhaustion attack; forced OPK reuse.
H-6 — console.log statements leak key material
- Files:
lib/e2ee/e2ee.js,lib/GroupMessageHandler.ts,lib/KEMTree.ts - Multiple
console.logcalls print raw key bytes, chain keys, or ratchet state during normal operation. In browser DevTools or server-side logs these are trivially readable. - Impact: Key material exposed to anyone with DevTools access or access to server log aggregation.
H-7 — getStaticProps leaks KEM tree state to unauthenticated page renders
- File: Next.js page files that call
getStaticPropswith thread/group data - Static-site generation may serialize and embed group tree metadata (member list, epoch, public keys) into the HTML payload served to all visitors before any auth check runs.
- Impact: Group membership and public key material exposed without authentication.
H-8 — Committed .env credentials
- File:
.env/.env.local(repository root) - Firebase API keys, project IDs, and potentially service-account credentials are committed to the repository. While Firebase client keys are low-sensitivity, service-account keys or admin SDK secrets in the same file would grant full backend access.
- Impact: Credential exposure to anyone with repository read access (public repo = world-readable).
H-9 — KEMTree.moveToIndex uses privateKey object as storage key name
- File:
lib/KEMTree.ts,moveToIndex - The storage key for updated tree nodes is built from
privateKey(aCryptoKeyobject), which coerces to"[object CryptoKey]", causing all nodes to collide on the same storage slot. The correct variable is the index integernewIndex. - Impact: KEMTree state corruption on every DH ratchet step; group decryption silently fails after the first epoch update.
M-1 — No Content Security Policy header
- Files:
next.config.js, deployment config - No
Content-Security-Policyheader is set. Combined with any XSS vector, an attacker can exfiltrate decrypted message content or key material to an external origin.
M-2 — 8-byte AES-GCM nonces in SecretTree
- File:
lib/SecretTree.ts - AES-GCM nonces are 8 bytes instead of the NIST-recommended 12 bytes. While not immediately broken, 8-byte nonces increase nonce-collision probability for high-volume senders and are rejected by some strict Web Crypto implementations (Node.js v20+).
M-3 — AAD binds only sender UID, not thread/epoch/message index
- File:
lib/e2ee/e2ee.js,lib/SecretTree.ts - Additional Authenticated Data passed to AES-GCM includes the sender UID but not the thread ID, epoch number, or message index. A ciphertext encrypted in thread A can be replayed into thread B without an authentication failure.
- Impact: Cross-thread replay attacks.
M-4 — SecretTree ratchet advances on failed decryption
- File:
lib/SecretTree.ts,getReceivingKey - The receiving chain key is advanced before decryption is attempted. A decryption failure (e.g. forged ciphertext) permanently advances the chain, desynchronizing the receiver from the sender. There is no rollback.
- Impact: Denial-of-service via chain corruption; one forged message permanently breaks the session.
M-5 — No out-of-order message support
- Files:
lib/SecretTree.ts,lib/MessageHandler.js - The group ratchet has no skipped-message-key cache. Out-of-order delivery (common in real networks) permanently desynchronizes the ratchet, requiring a full re-key to recover.
- Impact: Silent message loss under any network reordering.
M-6 — getAllUsers leaks all user profiles
- File:
firestore.rules/ API,getAllUsersquery - Any authenticated user can query the full
/userscollection with no pagination or field-level restrictions, exposing display names, public keys, and profile metadata for all registered users. - Impact: User enumeration; bulk public-key harvest for targeted attacks.
M-7 — File size validated after full read into memory
- File:
lib/GroupMessageHandler.ts, file-send path - File size is checked after the file has already been read into a buffer. A malicious 4 GB file will be fully loaded into the browser's memory before rejection.
- Impact: Client-side memory exhaustion / DoS.
M-8 — Passphrase stored/transmitted in plaintext input
- File: UI components (passphrase input fields)
- Identity key passphrases are handled as plaintext strings in React state before being passed to the KDF. They may be logged (see H-6), included in error messages, or visible in React DevTools.
- Impact: Passphrase exposure through developer tooling or error reporting.
M-9 — No SPK rotation schedule
- File:
lib/e2ee/e2ee.js,functions/index.js - Signed Pre-Keys are generated once and never rotated. Long-term SPK compromise retroactively breaks forward secrecy for all sessions established with that SPK.
- Impact: SPK compromise is permanent; no recovery path.
M-10 — canThread does not verify member consent
- File:
firestore.rules, thread-creation rule - A user can create a thread and add any other user as a member without that user's consent. The added user's client will then process key-material messages (ADDITION, WELCOME) sent by the creator.
- Impact: Unsolicited group addition; potential KEM tree injection.
M-11 — Debug / admin buttons present in production UI
- File: UI component files
- Several components render
clearAllDatabases,resetKeys, or similar admin-action buttons without an environment guard. These are visible and functional in production builds. - Impact: Any user (or XSS attacker) can wipe another user's local key store.
M-12 — clearAllDatabases exposed as a callable function with no auth
- File:
lib/e2ee/indexDB.js(or equivalent) - The
clearAllDatabasesfunction is exported and reachable from the UI without any authentication or confirmation step. - Impact: Irreversible local key deletion triggerable by any code with module access.
M-13 — No UPDATE authority check on group tree messages
- File:
lib/GroupMessageHandler.ts,handleMessage - When processing an
UNENCRYPTED_UPDATEorUNENCRYPTED_TREEmessage the handler does not verify whether thesentByuser is authorized to perform tree updates (e.g. only the group creator/admin). Any member can trigger an epoch rotation. - Impact: Epoch-rotation harassment; combined with C-6, full key compromise.
M-14 — Sequential key-package IDs are guessable
- File:
functions/index.js, key-package storage - Key packages are stored with predictable IDs (
kp1,kp2, …). An attacker who knows a user's UID can attempt to fetch key packages directly. - Impact: Key-package enumeration; facilitates OPK exhaustion (H-5).
L-1 — Hardcoded Firebase API key in client bundle
- File:
lib/firebase.ts/next.config.jsenv vars - The Firebase web API key is embedded in the client bundle. While Firebase API keys are intended to be public, the key grants access to all Firebase services; security relies entirely on Firestore/Storage rules being correct (which they are not — see Critical findings).
L-2 — auth.setPersistence(LOCAL) on shared devices
- File:
lib/firebase.ts LOCALpersistence stores the session token inlocalStorageindefinitely. On shared or public devices this leaves sessions open after the browser tab is closed.
L-3 — ESLint disabled in CI (--no-lint flag)
- File: CI config /
package.jsonbuild script - The production build skips linting, meaning security-sensitive lint rules (e.g. no-eval, no-dangerouslySetInnerHTML) are never enforced automatically.
L-4 — Static paths expose thread IDs
- File: Next.js page routing
- Thread IDs are used directly as URL path segments (e.g.
/chat/[threadId]). These IDs leak through browser history, server access logs, and referrer headers.
L-5 — No display-name length or character validation
- File: User profile update path
- Display names are stored without length or character set validation. Overly long names can overflow UI layout; names with HTML-like characters could interact with any future server-side rendering.
L-6 — No JSON schema validation on incoming Firestore messages
- File:
lib/GroupMessageHandler.ts,handleMessage - Messages are destructured from Firestore documents without schema validation. Unexpected or missing fields cause runtime TypeErrors that propagate as unhandled exceptions.
L-7 — Old epoch keys accumulate in IndexedDB
- File:
lib/e2ee/indexDB.js, epoch key management - Superseded epoch keys are not deleted after an epoch transition. Over time the local key store grows without bound and retains material that should be erased (violating the Key Erasure goal stated in this document).
L-8 — Missing await on OPK generation in some paths
- File:
functions/index.js/ client OPK generation - Several OPK generation calls lack
await, meaning the OPK may not be stored before the function returns success. This can result in an empty OPK pool for new sessions.
L-9 — verificationCeremony stores wrong key
- File:
lib/e2ee/e2ee.jsor equivalent - The key stored during the verification ceremony step uses the wrong variable name, potentially comparing a public key against itself rather than against the peer's key. The ceremony then always succeeds.
- Impact: Safety-number / key-verification feature provides no security guarantee.
L-10 — Zero old_init secret in generateFirstTimeSecrets
- File:
lib/KEMTree.ts - The initial commit secret (
old_init) is passed as a zero-filled buffer on first group creation. This is a well-known weak point; if the KEM tree KDF does not adequately mix the commit secret the initial epoch key has less entropy than intended.
L-11 — No CSRF protection on state-mutating API routes
- File: Next.js API routes (
pages/api/) - State-mutating API routes (key upload, profile update, etc.) do not validate a CSRF token or check the
Origin/Refererheader. Any page the user visits can silently submit cross-site requests.
L-12 — Unvalidated profile image URLs
- File: User profile update / display path
- Profile image URLs are stored in Firestore and rendered without validation that they point to the application's own Storage bucket. An attacker can store an external URL to track when users view another user's profile (pixel tracking).
L-13 — No rate limiting on Cloud Functions or Firestore writes
- File:
functions/index.js,firestore.rules - There is no per-user write-rate limit on message creation, OPK consumption, or friend-request generation. All endpoints are susceptible to low-effort denial-of-service.
L-14 — routeUser always sets valid = true regardless of auth state
- File:
lib/routeUser.js(or equivalent routing helper) - The routing helper that guards protected pages sets the
validflag totrueunconditionally in some code paths, meaning unauthenticated users can reach protected routes if the Firebase auth state resolves slowly.