This page is a technical supplement to the privacy explanation. It is written for engineers and security reviewers who want to understand the security model, the protocol surfaces, and what claims are and are not being made. The goal is not to market the system; the goal is to make its assumptions legible so they can be challenged.
The system is designed to protect message confidentiality and relationship privacy against an untrusted relay and an untrusted hosting provider. The relay is assumed to be able to observe network metadata (timing, request frequency, approximate volume, and IP-level information as any HTTPS service necessarily can), to store and replay ciphertexts, to drop or delay messages, and to provide malicious responses. The relay is not assumed to keep secrets, and the protocol does not rely on the relay for authentication, authorization, or recovery.
The system is additionally designed to tolerate an attacker who obtains a full copy of relay storage (mailbox documents containing routing channel identifiers and ciphertext payloads) without obtaining endpoint state. In that case, the attacker should not be able to decrypt message content or forge messages that will be accepted by recipients, because encryption keys and continuity secrets are never stored on the relay. Relay storage necessarily exposes per-relationship mailbox artifacts (distinct channel identifiers) and message volumes per mailbox, but the design aims to prevent cross-relationship linkage into a stable user graph because there is no global account identifier, directory entry, or reusable identity credential tying multiple relationships together at the protocol layer.
The system does not attempt to protect against an attacker who compromises an endpoint while the vault is unlocked, or an attacker who can directly observe the screen/keystrokes/OS environment. If an attacker controls your device or browser runtime, they can act as you for as long as that compromise persists. The system also does not claim to defeat traffic correlation by a global passive adversary; it aims to reduce linkability through per-relationship routing identifiers and cover traffic, but timing and volume metadata can still be exploited by powerful observers.
“Without identity” means there is no global account, username, phone number, email address, public profile, directory entry, or stable identifier that ties relationships together. The system does not register users. The relay cannot answer “who is this?” because the protocol never asks the relay to validate identity. Identity is local and pairwise: it exists only as the continuity of a specific relationship state between two parties.
“Keyless” does not mean the absence of secrets. Endpoints necessarily hold secret material to encrypt and authenticate messages. “Keyless” means there are no long-term, reusable asymmetric identity keypairs (no static signing keys, no PKI identity keys, no escrow keys, and no recoverable private keys) that can be used across relationships or that a server can validate. Instead of proving identity by presenting a stable credential, identity is proven by advancing a shared cryptographic history correctly. Keyless means no asymmetric identity keypairs or PKI; the prototype uses per-relationship symmetric secrets.
In other words: the protocol uses per-relationship, evolving secret state rather than a persistent identity credential. There is nothing a platform can look up and no master key that can be compelled, escrowed, rotated, or used to impersonate a user across contexts.
Each relationship maintains a pairwise continuity state. Conceptually, the continuity state is a shared, forward-advancing cryptographic chain. Every accepted message must advance that chain by one valid step under a strict continuity acceptance rule: the receiver only advances when the message’s sender_prev_ddh matches the receiver’s current expected tip, subject to a narrow recent-tip tolerance described below. Messages that are replayed, reordered, spliced, or forged fail because they cannot satisfy the step rule relative to the receiver’s state.
Messages are encrypted and authenticated using direction-specific secrets. Direction-specific means Alice→Bob uses a different evolving secret than Bob→Alice. This prevents reflection and cross-direction replay classes and allows each direction to advance independently.
The relay sees only (i) a routing identifier used for mailbox selection (a channel identifier) and (ii) opaque ciphertext. The relay cannot compute valid future ciphertexts, cannot mutate continuity state, and cannot cause a recipient to accept a forged message, because acceptance requires knowledge of the current continuity state that exists only on the endpoints.
A connection code is a base64url-encoded invitation object used to initialize a new relationship. It is intentionally not a username and it is not a stable identity claim. It is a capability that carries the minimum information required to let a recipient establish initial pairwise state and determine the two mailbox routing identifiers that will be used for the relationship.
Bootstrap authenticity depends on the out-of-band channel used to share the invite; the relay is not trusted during bootstrap. Anyone who obtains a connection code can consume it. This is by design. After a connection is established, the parties should confirm they are speaking with the intended counterparty through an out-of-band check if the environment is high risk.
Single-use semantics are enforced only locally per device in this prototype. The accepting device records the invite ID as “used” in its local vault. There is no global single-use enforcement without introducing an online authority, and the inviter is not automatically notified that an invite was consumed.
At a minimum, each delivered message includes (i) a routing identifier (channel identifier / mailbox), (ii) a ciphertext payload, and (iii) enough protocol metadata for the recipient to validate that the message represents a valid next continuity step under the receiver’s current state.
Acceptance uses a strict continuity invariant: an inbound message is considered for acceptance only if its sender_prev_ddh equals the recipient’s current expected tip for that direction, or equals a recently accepted tip in a bounded recent-tip window. The recent-tip window exists to tolerate limited out-of-order delivery of epoch-stable continuity steps (cover/checkpoint/control) and to allow the next real message at the expected epoch when its sender_prev_ddh corresponds to a recently accepted tip (indicating that epoch-stable steps advanced continuity between the last real message and the current real message).
If validation succeeds, the recipient atomically advances the continuity tip and then processes the decrypted payload. Replays fail because the state has already advanced. Reordering fails because only a valid next-step relative to the expected tip (or bounded recent-tip tolerance) is accepted. Forgery fails because producing a valid next step requires knowledge of the per-relationship secret state.
The protocol duplicates the continuity token in the transport header and inside the encrypted payload. This is deliberate: it enables fast pre-decryption screening (and deterministic AAD binding), binds transport metadata to semantic payload state, and ensures relay-layer manipulation cannot change continuity metadata without detection.
On receipt, ciphertext authentication binds to the locally configured inbound mailbox identifier for the relationship direction (the receiver’s recv_channel_id), rather than trusting any relay-provided routing fields. This prevents a malicious relay from influencing AAD verification by mutating or substituting routing metadata in the relay document.
The receiver buffers relay documents per inbound mailbox and sorts candidates by sender_epoch as a processing heuristic. Acceptance is still strictly gated by continuity rules, so out-of-order or gap cases may remain buffered while recovery is requested. The receiver does not assume the relay preserves ordering. Relay documents are deleted only after successful processing or after being classified as duplicate or stale under the receiver rules.
Once continuity is established, inbound messages whose sender_epoch is strictly less than the last accepted peer_sender_epoch_last are treated as obsolete and are deleted without recovery. This hard-drop rule removes stale bootstrap traffic and avoids repeated work on provably old epochs.
Epoch-stable messages (cover/checkpoint/control) are permitted to occur at the current baseline epoch while still advancing continuity. If an epoch-stable message arrives late after the receiver tip has already advanced at the same baseline epoch (valid header/rule but sender_prev_ddh does not match the current strict tip), the receiver treats it as stale and deletes it as replay-like cleanup rather than escalating to a fork state.
In addition to commitment-based replay suppression, the receiver performs explicit replay cleanup when a decrypted payload’s sender_ddh_payload exactly equals the last accepted peer_sender_last for that direction. Such messages do not advance the receiver continuity tip and are deleted as idempotent duplicates.
Before the first real message is accepted, the receiver treats the baseline epoch as 0 and permits epoch-stable bootstrap traffic at epoch 0. The first real message is expected at epoch 1.
Relay-only compromise (attacker obtains relay storage): the attacker should not be able to decrypt message bodies or forge messages that recipients will accept, because per-relationship secrets and continuity state are never stored on the relay.
Locked-vault compromise (attacker obtains local vault blobs while locked): security reduces to the PIN-derived vault wrapping key. The implementation uses PBKDF2-HMAC-SHA256 with an iteration count chosen at vault creation time based on device/browser capability. The target iteration count begins at 600,000, and creation may fall back down to a minimum of 100,000 if the device cannot derive at the higher count. Unlock uses the stored iteration count and will fail rather than falling back. A weak PIN materially reduces security in this compromise scenario.
Unlocked-endpoint compromise (attacker compromises browser/OS while unlocked): the attacker may read plaintext and may impersonate the user for as long as the compromise persists, because the required per-relationship secrets and current continuity state exist in-process while unlocked. Locking the vault stops relay listeners and clears the in-memory master secret.
Relationship-secret compromise (attacker obtains per-relationship secrets plus relevant continuity tip): the attacker can compute valid future continuity steps for that direction until the relationship is re-established. This prototype does not yet implement an independent post-compromise recovery ratchet; it prioritizes removal of long-term identity keypairs and server authority over PCS-style guarantees.
The relay can drop messages, delay messages, reorder delivery attempts, replay ciphertexts it has already seen, and observe traffic metadata. The relay cannot decrypt message content, cannot derive future encryption keys, and cannot forge messages that recipients will accept, assuming endpoint state remains uncompromised and the cryptographic primitives are implemented correctly.
If the relay is fully compromised, it can still disrupt availability. This system prioritizes confidentiality and non-impersonation over guaranteed delivery. Availability can be improved with redundancy, multiple relays, or client-side retry logic, but those features do not change the core security model.
The system reduces linkability by using per-relationship mailbox identifiers, avoiding global accounts, and sending background cover traffic. However, the relay and network observers may still infer metadata from timing, volume, and network paths. Cover traffic raises the cost of inference; it does not claim to make inference impossible against a global observer.
If you need stronger network-level unlinkability, you may route traffic through anonymizing networks (e.g., Tor) and/or use multiple relays, mix networks, and batching strategies. These are orthogonal layers.
Groups are not treated as a shared cryptographic object. There is no shared group key, no group membership list on the server, and no group state that must be synchronized. Send to group is implemented as multiple one-to-one sends, preserving pairwise continuity guarantees and preventing a recipient from learning who else received the same message.
Because there are no accounts and no server-validated credentials, there is also no account recovery. If local relationship state is erased, that relationship cannot be resumed; it must be re-established via a new connection code. This is a deliberate tradeoff: recovery typically requires a party that can reassert identity, which this design forbids.
This system makes strong claims about removing platform-validated identity credentials and about server blindness with respect to message content, but it necessarily relies on endpoint security. It does not prevent traffic analysis by powerful observers. It does not protect against device compromise while unlocked. It does not prevent an attacker who steals a connection code from consuming it. These are intrinsic tradeoffs in exchange for eliminating central authority, eliminating global identity, and removing reusable long-term asymmetric identity keypairs.
If you are reviewing this design, the most valuable questions to ask are: what is the exact bootstrap mechanism and its out-of-band authenticity assumptions; what is the exact state-advance rule; what primitives are used (hash/KDF/AEAD); how the out-of-order tolerance rules interact with epoch-stable continuity steps; and what the guarantees are under partial endpoint compromise and later recovery. Those details determine the security envelope.
This section documents the protocol as it is implemented in the current prototype. The implementation uses browser WebCrypto primitives via helper functions in $lib/aqcrypto. Field names below match the code paths in src/lib/state/contacts.ts, src/lib/state/conversations.ts, and src/lib/state/session.ts.
The implementation uses a per-relationship continuity token (called DDH in the code) to represent the forward-advancing chain tip used to validate
continuity for a specific direction within a specific relationship. DDH here is not a device identifier.
A new relationship is bootstrapped using a base64url-encoded JSON invite object (InviteV1). The invite is generated by createInviteCode() and consumed by acceptInviteCode(). The invite is not a stable identity claim. It is a capability that carries
per-relationship secrets, an expiration timestamp, and an invite ID.
The invite payload fields are: v (version, currently 1), label (human label), exp (expiration timestamp, currently 30
minutes), a_inbox_secret_hex (32 random bytes hex), b_inbox_secret_hex (32 random bytes hex), and id (random 16 bytes
hex).
Each side stores two 32-byte secrets: a send secret and a receive secret. When one party creates an invite, they store send_secret_hex = b and recv_secret_hex = a, and the accepter stores send_secret_hex = a and recv_secret_hex = b. This yields symmetric
pairwise state without accounts, directories, or global identity keys.
Invite reuse is blocked only locally on the accepting device by storing used invite IDs in the local vault. There is no global enforcement without introducing an online authority.
Routing uses per-relationship mailbox identifiers derived from the mailbox secret (not from a user identity). The derivation is: channel_id = SHA-256("aq-mbx|" + secretHex). In the code, these are stored as send_channel_id and recv_channel_id on
each contact. The relay stores mailbox documents keyed by a Firestore document ID, each containing the derived channel_id plus opaque ciphertext
fields. The relay never receives the underlying secrets, only the derived channel IDs.
In this prototype, mailbox IDs are stable per relationship (they do not rotate automatically). Rotation could be added by minting new per-relationship mailbox secrets and migrating state, but that is not implemented here.
The implemented primitives (as used in the protocol) are: SHA-256 (hash commitments and AAD binding), HMAC-SHA256 (continuity step derivation and genesis), HKDF (derivation of message encryption keys and vault keys), AES-256-GCM (message encryption and vault sealing), and PBKDF2-HMAC-SHA256 (PIN-based KEK derivation for vault wrap).
Each relationship maintains a sender-side continuity chain for outbound messages and a receiver-side continuity chain for inbound verification. The chain tip is
stored as my_sender_curr for outbound, and the last accepted peer tip is stored as peer_sender_last for inbound. The initial genesis
tip is computed as: genesis_ddh = HMAC-SHA256(secret, "aq-genesis|v2").
Each outbound send advances the continuity tip using an HMAC step rule. The sender chooses a fresh per-message step salt (stepSaltHex, 16 random
bytes hex), determines a rule tag ("cover" or "message"), and computes the next tip as: sender_curr = HMAC-SHA256(send_secret, "aq-id-step|v2|" + ruleTag + "|" + sender_prev + "|" + stepSaltHex). This step is performed for every
message type (real, cover, checkpoint, control). A recipient recomputes this same rule from the received sender_prev_ddh and sender_step_salt and accepts only if it matches the transmitted sender_ddh_payload, which is mirrored into the relay header as sender_ddh_header.
The protocol authenticates “who sent this” by continuity: only a sender holding the relationship secret and the correct previous tip can produce the next valid tip. There is no persistent signing keypair and no server-verified identity credential.
The implementation distinguishes a logical epoch counter (sender_epoch) from the continuity tip. The epoch advances only for kind === "real" messages. Epoch-stable traffic ("cover", "checkpoint", and "control") does not increment
epoch, but it still advances the DDH continuity tip.
Receiver validation enforces: for real messages, msg.sender_epoch must equal the expected next epoch (or be a future-gap case that triggers
recovery); for epoch-stable messages, msg.sender_epoch must match the current baseline epoch (with epoch 0 permitted for first-contact bootstrap).
Continuity is enforced by strict previous-tip matching, with a bounded recent-tip window used to tolerate missed/out-of-order epoch-stable steps and to allow
the next real message when its sender_prev_ddh corresponds to a recently accepted tip.
Each posted relay document includes: channel_id, sender_ddh_header, sender_epoch, ct_hex, iv_hex, kdf_salt_hex, and a client_nonce.
Encryption uses per-message keys derived by HKDF from the per-relationship secret and a fresh random salt. On the sender, ikm = send_secret_hex;
on the receiver, ikm = recv_secret_hex. The sender generates kdfSalt = random 16 bytes and computes: key = HKDF(ikm, kdfSalt, info="aq-msg-v1"). The message uses AES-256-GCM with a fresh random 12-byte IV.
Additional authenticated data (AAD) is mandatory and binds ciphertext to continuity metadata, epoch, and the mailbox identifier for that direction. The sender
computes: AAD = SHA-256("aq-aad|v1|" + sender_curr + "|" + sender_epoch + "|" + send_channel_id). The receiver recomputes AAD using its inbound
mailbox identifier recv_channel_id, which is the same mailbox channel identifier as the sender’s send_channel_id for that direction.
Receiver-side verification does not trust relay-provided channel_id fields for AAD recomputation; it binds to the locally configured recv_channel_id.
The encrypted payload is JSON (padded; see below) that includes, at minimum: v, kind, ts, sender_ddh_payload, sender_prev_ddh, sender_step_salt, sender_epoch, bootstrap, and commitment_hex.
The payload commitment is: commitment_hex = SHA-256("aq-commit|v1|" + sender_curr + "|" + sender_epoch + "|" + send_channel_id). The receiver
maintains a per-inbound-channel commitment cache to suppress accepting the same ciphertext/payload again even if relay snapshot behavior causes duplicate
deliveries. In the current prototype, commitment suppression uses SEEN_COMMITMENT_TTL_MS = 10 minutes.
Checkpoint messages additionally include: checkpoint_anchor = SHA-256("aq-ckpt|v1|" + sender_curr + "|" + sender_epoch). The receiver recomputes
this anchor and rejects the checkpoint if it does not match.
Network payloads use a 2-byte length prefix with random tail padding. Real messages use bucketed padding (encodeBucketedJson) to the nearest
512-byte bucket up to an 8192-byte cap. Cover-like messages attempt to pad to a fixed 256-byte size; if the payload does not fit, the encoder falls back to a
length-prefixed JSON payload without the fixed bucket size.
Local vault persistence is implemented in multiple modules and is not uniform across all stored blobs. In conversations.ts, persisted transcripts,
unread maps, outbox, and pending queues are sealed under the vault master using HKDF-derived AES-GCM keys and are bucket-padded to 4096 bytes to reduce
plaintext-size leakage via ciphertext length in localStorage. Other vault blobs (e.g., contacts and used-invite records maintained in contacts.ts) may be sealed without bucket padding, depending on the module implementation.
The prototype emits cover traffic on a per-contact timer. Cover sends occur at random intervals between 15 seconds and 45 seconds
(COVER_MIN_MS to COVER_MAX_MS). Cover traffic advances the sender DDH tip but does not advance epoch.
Additionally, the prototype sends an explicit checkpoint every 10 real epochs (CHECKPOINT_EVERY_EPOCHS = 10) at the boundary moment to avoid the
possibility that randomized cover timing skips the checkpoint boundary. Checkpoints are epoch-stable (do not increment epoch) but advance the DDH tip and
include a checkpoint anchor. The receiver records checkpoints as recovery diagnostics and may seed recent continuity tips after unlock using the last known
checkpoint DDH.
Incoming relay documents are buffered per inbound mailbox (recv_channel_id) and drained using continuity checks. A relay document is deleted only
after successful processing or after being classified as duplicate or stale. Duplicate suppression uses two mechanisms: (i) a per-channel processed-doc-id TTL
cache (PROCESSED_TTL_MS = 10 minutes) to suppress Firestore snapshot churn and repeated attempts to process the same relay doc, and (ii) a per-channel
commitment cache (SEEN_COMMITMENT_TTL_MS = 10 minutes) to avoid accepting the same ciphertext/payload again.
To tolerate limited out-of-order epoch-stable traffic and to bridge over missed epoch-stable steps, the receiver maintains a recent-tip window per inbound
mailbox: RECENT_TIPS_MAX = 32. The window is seeded after unlock from the last known peer checkpoint DDH when available.
A message is accepted only if: the header matches the payload continuity token (sender_ddh_header === sender_ddh_payload); the continuity step rule
matches the payload token (recomputed HMAC rule equals sender_ddh_payload); the epoch rule is satisfied for the message kind; and the continuity
predecessor condition is satisfied (payload sender_prev_ddh matches the receiver’s strict current tip, or in the narrow “bridge” case for a next-epoch
real message, matches an allowed recent tip). Accepted steps always advance the receiver’s DDH continuity tip. For real messages, the receiver advances to the
next epoch; for epoch-stable messages (cover/checkpoint/control), the receiver remains at the current baseline epoch while still advancing the DDH tip.
Future-gap detection is explicit: if header and rule checks pass but the sender_epoch is greater than the expected next epoch, the receiver treats this as a future epoch gap, marks the conversation recovering, and issues a recovery request. A distinct case exists where a message at the expected next real epoch passes header and rule checks but fails the predecessor condition; this is treated as “missed epoch-stable steps” and triggers recovery while keeping the real message buffered.
Recovery uses an outbox retransmit mechanism. The sender stores an outbox of recently posted envelopes and can retransmit a contiguous chain window beginning at the envelope whose sender_prev_ddh matches the peer’s last known tip. This allows the receiver to fill missing continuity steps and then accept buffered messages.
Groups are implemented as UI-level fan-out: a group send produces multiple pairwise sends. There is no group key, no server-side group roster, and no shared group identity object. Recipients can observe an optional group tag in payload metadata, but this does not imply membership or reveal the recipient list.
Relationship secrets, continuity tips, transcripts, and outbox envelopes are stored locally and sealed when the session vault is unlocked. The vault is wrapped with
a PIN-derived KEK: PBKDF2(pin, salt, iter) derives an AES-GCM key that encrypts a randomly generated 32-byte vault master secret. While unlocked, the
vault master is held in-memory only. On lock, relay listeners are stopped and the in-memory master is cleared.
PBKDF2 iteration policy in the current implementation begins at 600,000 iterations and may fall back down to a minimum of 100,000 only during vault creation. The chosen iteration count is stored in vault metadata and is reused on unlock; unlock does not perform fallback.
Because there is no central authority and no account recovery, if local relationship state is erased, the relationship cannot be resumed and must be re-established with a new invite. This is an explicit tradeoff to eliminate platform recoverability and long-term identity keypairs.
To prevent accidental continuity forks caused by multiple open windows in the same browser profile, the sender uses a best-effort local lease (stored in localStorage)
keyed by send_channel_id. Only the lease owner is allowed to advance the sender chain; other windows will treat this as a fork and block sending. The
lease has a TTL of 45 seconds and is renewed while sending. This is a same-browser-profile safety mechanism; it is not a multi-device synchronization protocol.
If you are reviewing the security properties, the highest-value questions are: whether the continuity rule (including strict previous-tip semantics and the recent-tip bridge behavior), epoch rules, and AAD binding admit any forgery, splice, downgrade, or replay classes; whether the bootstrap invite capability is appropriately scoped under the stated out-of-band authenticity assumptions; whether the future-gap and missed-epoch-stable recovery triggers can be induced by an adversarial relay to create denial-of-service or persistent recovery loops; and whether the cover/checkpoint behavior produces unintended distinguishers. The prototype is explicit about non-goals (global passive adversary unlinkability and endpoint-compromise resistance while unlocked).