Skip to Content
Cryptography Fundamentals

Cryptography Fundamentals

Deep explanation of the cryptographic primitives used in Zentalk.


Why Cryptography Matters

Every message you send could be intercepted. Without cryptography:

  • Network operators see everything
  • Servers read your messages
  • Attackers steal your data

Zentalk uses multiple layers of cryptography to ensure that only you and your recipient can read messages.


Diffie-Hellman Key Exchange

The Problem

Alice wants to talk to Bob securely. But how do they agree on a secret key if anyone can listen to their conversation?

The naive approach fails:

Alice: "Let's use password 'secret123'" Eve (attacker): *intercepts* "Now I know the password too"

The Solution: Diffie-Hellman

Diffie-Hellman (DH) lets two parties create a shared secret even if an attacker sees all communication.

How it works:

StepAliceBobEve sees
1Picks secret aPicks secret bNothing
2Computes A = g^a mod pComputes B = g^b mod p-
3Sends A to BobSends B to AliceA and B
4Computes B^a = g^(ab)Computes A^b = g^(ab)Cannot compute g^(ab)

Result: Alice and Bob both have g^(ab) but Eve only has A, B, g, and p.

Why Eve Cannot Compute the Secret

To find g^(ab) from A = g^a and B = g^b, Eve would need to solve:

Given: g^a and g^b Find: g^(ab)

This is the Computational Diffie-Hellman Problem (CDH). With proper parameters, this requires approximately 2^128 operations - more than all computers on Earth could perform in the age of the universe.

X25519: Modern Diffie-Hellman

Zentalk uses X25519, a modern elliptic curve variant:

PropertyValue
CurveCurve25519 (Montgomery)
Field size2^255 - 19
Security128-bit equivalent
Key size32 bytes

Why X25519 over classical DH:

  • Faster: 10-100x faster than RSA-based key exchange
  • Smaller keys: 32 bytes vs 256+ bytes for RSA
  • Constant-time: Resistant to timing attacks
  • Simple: Fewer implementation pitfalls

Mathematical operation:

Alice's secret: a (256-bit random number) Alice's public: A = a × G (point multiplication on Curve25519) Bob's secret: b Bob's public: B = b × G Shared secret = a × B = b × A = ab × G

Both compute the same point on the curve, which becomes the shared secret.


X3DH: Extended Triple Diffie-Hellman

The Problem X3DH Solves

Basic Diffie-Hellman requires both parties to be online simultaneously. But messaging apps need asynchronous communication - Alice should be able to send a message even if Bob is offline.

Key Bundle

Bob publishes keys to a server before Alice contacts him:

KeyTypePurposeLifetime
Identity Key (IK)Ed25519Identifies BobPermanent
Signed Pre-Key (SPK)X25519Enables key exchange7 days
One-Time Pre-Keys (OPK)X25519Extra forward secrecySingle use

Why Four DH Computations?

X3DH performs four separate DH operations:

DH1 = X25519(IK_Alice, SPK_Bob) DH2 = X25519(EK_Alice, IK_Bob) DH3 = X25519(EK_Alice, SPK_Bob) DH4 = X25519(EK_Alice, OPK_Bob)

Each DH provides different security properties:

DHAlice usesBob usesSecurity Property
DH1Identity KeySigned Pre-KeyAuthenticates Alice to Bob
DH2Ephemeral KeyIdentity KeyAuthenticates Bob to Alice
DH3Ephemeral KeySigned Pre-KeyForward secrecy (weekly)
DH4Ephemeral KeyOne-Time KeyForward secrecy (per message)

Why all four are needed:

  • DH1 alone: No forward secrecy (if IK_Alice leaked, all past secrets revealed)
  • DH2 alone: No forward secrecy (if IK_Bob leaked, all past secrets revealed)
  • DH3 alone: No authentication (attacker could impersonate)
  • DH4 alone: No authentication, only single-use

Combined: Authentication + Forward Secrecy + Single-use protection

Key Derivation

The four DH outputs are combined using HKDF:

SK = HKDF(DH1 || DH2 || DH3 || DH4, salt=0, info="ZentalkX3DH")

Why concatenate all four?

  • An attacker must break all four DH computations to recover SK
  • Even if one key is compromised, the others protect the secret
  • HKDF extracts entropy from all inputs uniformly

HKDF: Key Derivation Function

Why We Need Key Derivation

Raw DH outputs are not suitable as encryption keys:

  • Non-uniform distribution (some bit patterns more likely)
  • No domain separation (same secret used for different purposes)
  • No key expansion (need multiple keys from one secret)

How HKDF Works

HKDF has two phases: Extract and Expand.

Extract Phase:

PRK = HMAC-SHA256(salt, input_key_material)
  • Takes non-uniform input (DH output)
  • Produces uniformly random pseudorandom key (PRK)
  • Salt adds additional randomness

Expand Phase:

OKM = HMAC-SHA256(PRK, info || 0x01) || HMAC-SHA256(PRK, T1 || info || 0x02) || ...
  • Expands PRK to arbitrary length
  • info parameter provides domain separation

Domain Separation

The info parameter ensures keys for different purposes are independent:

UsageInfo StringResult
X3DH”ZentalkX3DH”Initial shared secret
Chain key”ZentalkChain”Message key derivation
Ratchet”ZentalkRatchet”DH ratchet keys
Hybrid”ZentalkHybrid”Post-quantum combination

Why this matters:

  • Even with the same input, different info produces different keys
  • Compromise of one key doesn’t reveal others
  • Prevents cross-protocol attacks

Double Ratchet Algorithm

The Problem

X3DH creates one shared secret. But we need:

  • A new key for every message (forward secrecy)
  • Recovery if a key is compromised (post-compromise security)

Two Ratchets

The Double Ratchet combines two mechanisms:

1. Symmetric Ratchet (KDF Chain)

For each message: Message_Key, Chain_Key_new = HKDF(Chain_Key, "ZentalkChain") Encrypt message with Message_Key Delete Message_Key immediately Chain_Key = Chain_Key_new

Why this provides forward secrecy:

  • Each Message_Key is derived from Chain_Key
  • After derivation, previous Chain_Key is deleted
  • Attacker who captures Chain_Key at time T cannot derive keys from time T-1

2. DH Ratchet (Asymmetric)

When receiving new DH public key from peer: DH_output = X25519(my_private, their_new_public) Root_Key_new, Chain_Key_new = HKDF(Root_Key, DH_output) Generate new DH keypair for myself

Why this provides “healing”:

  • Fresh DH exchange creates completely new key material
  • Even if Chain_Key was compromised, new DH recovers security
  • Attacker must maintain continuous access to break future messages

Visual: How Keys Flow

Root_Key ──HKDF──► Root_Key_new ──HKDF──► ... │ │ └──► Chain_Key └──► Chain_Key │ │ ├──► MK1 ├──► MK4 ├──► MK2 ├──► MK5 └──► MK3 └──► MK6

Each message uses a unique Message Key (MK). Compromising MK3 reveals nothing about MK1, MK2, MK4, etc.

Forward Secrecy Timeline

EventAttacker learnsCan decrypt
Captures MK5MK5 onlyMessage 5 only
Captures Chain_Key at step 5MK5, MK6, MK7…Messages 5+ until DH ratchet
Captures Root_KeyFuture chainsUntil peer sends new DH
Captures Identity KeyNothing directlyMust also break session

Key insight: The more frequently DH ratchets occur (more messages exchanged), the smaller the window of compromise.


Forward Secrecy Analysis

This section provides a rigorous analysis of forward secrecy guarantees, compromise windows, and practical limitations of the Double Ratchet and Sender Keys protocols.

What Forward Secrecy Actually Guarantees

Precise Definition: Forward secrecy ensures that compromise of long-term keys at time T does not reveal session keys (and thus messages) from time T-1 or earlier.

However, this definition requires careful qualification:

Forward Secrecy ≠ "Past messages are always safe" Forward Secrecy = "Compromise of THIS key doesn't reveal THOSE messages"

The crucial distinction: Forward secrecy protects against future compromise of current keys, not against current compromise of current keys. If an attacker obtains your Chain Key right now, they can derive all future Message Keys in that chain until a DH ratchet occurs.

Levels of Key Compromise

Different keys have different compromise impacts:

Key TypeDerivationLifetimeCompromise Impact
Message KeyFrom Chain KeySingle messageMinimal - one message
Chain KeyFrom Root Key + DHUntil DH ratchetMedium - messages in chain
Root KeyFrom previous Root + DHUntil DH ratchetHigh - current + future chains
Identity KeyGenerated oncePermanentCritical - future sessions

Compromise Window Analysis

Detailed breakdown of what each key compromise reveals:

Key CompromisedMessages ExposedRecovery MechanismTime to Recovery
Message Key (MK)1 message onlyAutomatic (MK deleted after use)Immediate
Chain Key (CK)All messages until next DH ratchetNext reply from conversation peerVariable (depends on peer activity)
Root Key (RK)Current chain + all future chainsNext DH ratchet with new peer keyVariable (depends on peer activity)
Identity Key (IK)Future sessions only (not past)Manual key rotationUser action required

Mathematical representation:

Let CK_n be the Chain Key at step n. An attacker with CK_n can compute:

CK_{n+1}, MK_n = HKDF(CK_n) CK_{n+2}, MK_{n+1} = HKDF(CK_{n+1}) ...and so on

But cannot compute:

CK_{n-1} (one-way function) MK_{n-1} (deleted)

DH Ratchet Frequency

When does a DH ratchet occur?

The DH ratchet advances when a party receives a message with a new DH public key from their peer. This happens when:

  1. Alice sends a message (includes her new DH public key)
  2. Bob receives it and performs DH ratchet
  3. Bob sends a reply (includes his new DH public key)
  4. Alice receives it and performs DH ratchet

Critical observation: The DH ratchet requires bidirectional communication.

One-Sided Conversation Problem

Scenario: Alice sends 1,000 messages to Bob without any reply.

Message 1: Alice → Bob (Alice's DH key: A₁) Message 2: Alice → Bob (Alice's DH key: A₁) ← Same DH key! Message 3: Alice → Bob (Alice's DH key: A₁) ... Message 1000: Alice → Bob (Alice's DH key: A₁) All 1000 messages use the same sending chain!

Consequence: If Alice’s sending Chain Key is compromised at message 500:

  • Messages 500-1000 can be decrypted
  • Messages 1-499 are safe (MKs deleted, CK one-way)

Worst case exposure:

ScenarioMessages at Risk
Normal conversation (alternating)1-2 messages per chain
Monologue (no replies)Unlimited until reply
Group broadcastAll messages since last member change

Quantitative Analysis

Average messages per ratchet step (typical conversation):

E[messages per chain] = 1 / P(peer replies)

For a balanced conversation where each party has 50% chance of the next message:

E[messages per chain] ≈ 2

For a support chat where user sends 80% of messages:

E[messages per chain for user] ≈ 1.25 E[messages per chain for support] ≈ 5

Time window of exposure:

Conversation PatternAvg Chain LengthTypical Time Window
Active chat (both typing)1-2 messagesSeconds
Async messaging3-5 messagesMinutes to hours
Announcements (one-way)UnboundedDays to weeks
Dormant conversation1 messageUntil next exchange

Comparison with Other Protocols

ProtocolForward Secrecy GranularityRecovery Mechanism
Signal (Double Ratchet)Per-chain (requires reply)DH ratchet on reply
Matrix (Megolm)Per-sessionNew session creation
WhatsApp (Signal)Per-chainDH ratchet on reply
TLS 1.3Per-sessionNew connection
OTRPer-message (with ACK)Every message exchange

Signal/Zentalk vs OTR:

  • OTR provides per-message forward secrecy but requires acknowledgments
  • Signal optimizes for async by batching ratchets with replies
  • Trade-off: Signal has larger compromise windows but works offline

Sender Keys Limitation (Group Chats)

Group messaging uses a fundamentally different model:

Alice's Sender Key → Encrypts ALL of Alice's messages to group Bob's Sender Key → Encrypts ALL of Bob's messages to group

Critical limitation: There is no per-message forward secrecy in group chats.

Individual ChatGroup Chat
Per-message keysPer-sender key
DH ratchet on replyNo ratchet mechanism
Compromise exposes chainCompromise exposes ALL future messages from sender
Automatic recoveryRecovery only on member change

Group compromise scenario:

1. Alice joins group at time T₀ 2. Alice's Sender Key is compromised at time T₁ 3. Attacker can decrypt: - All of Alice's messages from T₁ onwards - NOT other members' messages - NOT Alice's messages before T₁ 4. Recovery: Alice must leave and rejoin (new Sender Key) OR group membership must change (forces rekey)

Mathematical representation:

For a sender key SK compromised at message n:

Exposed: Decrypt(SK, message_n), Decrypt(SK, message_{n+1}), ... Safe: message_1 through message_{n-1} (forward secrecy preserved)

Sender Key Rotation Policies

When Sender Keys rotate:

EventRotationRationale
Member joinsAll Sender KeysNew member shouldn’t read history
Member leavesAll Sender KeysLeaving member shouldn’t read future
Periodic timerOptionalLimit exposure window
Manual requestSingle Sender KeyUser-initiated recovery

Recommended rotation frequency:

Rotation interval = min( max_messages_per_sender, max_time_since_rotation, any_membership_change ) Suggested values: - max_messages_per_sender: 1000 - max_time_since_rotation: 7 days

Attack Scenarios and Mitigations

Scenario 1: Chain Key Theft

Attack: Malware extracts Chain Key from memory Exposed: ~N messages (N = messages until peer replies) Mitigation: Encourage frequent replies, periodic "ping" messages

Scenario 2: Long-term Key Compromise

Attack: Identity Key stolen via device seizure Exposed: All future sessions (past sessions safe) Mitigation: Safety number verification, key rotation capability

Scenario 3: Group Sender Key Theft

Attack: Single group member's device compromised Exposed: All future messages from that member Mitigation: Periodic forced rotation, membership change triggers

Recommendations for Maximum Security

For Individual Conversations:

  1. Encourage back-and-forth messaging

    • Each reply triggers a DH ratchet
    • Smaller compromise windows
  2. Send periodic “ping” messages

    • For one-sided communications, receiver should acknowledge
    • Even an empty acknowledgment advances the ratchet
  3. Verify Safety Numbers

    • Detects MITM attacks on key exchange
    • Should be done out-of-band

For Group Chats:

  1. Limit group size

    • Fewer members = fewer potential compromise points
    • Consider splitting large groups
  2. Implement rotation policies

    • Time-based: Rotate every 7 days
    • Volume-based: Rotate every 1000 messages
    • Event-based: Rotate on any membership change
  3. Use ephemeral messages

    • Auto-delete reduces value of key compromise
    • Complements but doesn’t replace forward secrecy
  4. Audit group membership regularly

    • Remove inactive members
    • Each removal triggers security recovery

Summary: What’s Protected vs What’s Not

Protected (Forward Secrecy Works)Not Protected (Limitations)
Past messages (MKs deleted)Future messages in same chain
Messages before Chain Key compromiseMessages after CK compromise, before ratchet
Other parties’ Sender KeysYour Sender Key (groups)
Past sessions with compromised IKFuture sessions with compromised IK

Key takeaway: Forward secrecy is not absolute. Understanding its boundaries helps users make informed security decisions about when to force ratchets, rotate keys, and verify security.


AES-256-GCM: Authenticated Encryption

Why Encryption Alone Is Not Enough

Basic encryption (like AES-CBC) only provides confidentiality:

Attacker intercepts: E(key, "Transfer $100 to Alice") Attacker modifies ciphertext bits Result might decrypt to: "Transfer $900 to Alice"

Without authentication, attackers can modify ciphertexts even without knowing the plaintext.

Galois/Counter Mode (GCM)

GCM provides both encryption AND authentication in one operation:

ciphertext, tag = AES-GCM-Encrypt(key, nonce, plaintext, associated_data)
ComponentPurpose
CiphertextEncrypted message (confidentiality)
TagAuthentication code (integrity)
Associated DataAuthenticated but not encrypted

How GCM Works

Counter Mode Encryption:

Counter_0 = Nonce || 0x00000001 Counter_1 = Nonce || 0x00000002 ... Ciphertext = Plaintext XOR AES(Key, Counter_1) || Plaintext XOR AES(Key, Counter_2) || ...

GHASH Authentication:

Tag = GHASH(H, Associated_Data || Ciphertext || lengths) where H = AES(Key, 0)

GHASH is a polynomial evaluation in GF(2^128) - very fast, especially with hardware support.

Nonce Requirements

Critical: The nonce must never repeat with the same key.

PropertyValue
Nonce size96 bits (12 bytes)
GenerationRandom or counter
Collision probability2^-48 after 2^48 messages

What happens if nonce repeats:

  • XOR of two plaintexts is revealed
  • Authentication key H can be recovered
  • All past and future messages with this key become vulnerable

Zentalk’s approach: Random 96-bit nonce per message. With 2^48 messages before 50% collision probability, this is safe for any realistic message volume.

Associated Data in Zentalk

AD = version || sender_id || recipient_id || timestamp

Why these fields:

  • version: Prevents downgrade attacks
  • sender_id: Ensures message from claimed sender
  • recipient_id: Prevents message redirection
  • timestamp: Prevents replay attacks

If an attacker modifies ANY of these fields, decryption fails.


Ed25519: Digital Signatures

What Signatures Provide

PropertyMeaning
AuthenticationMessage came from claimed sender
IntegrityMessage was not modified
Non-repudiationSender cannot deny sending

How Ed25519 Works

Ed25519 is based on the Edwards curve:

-x² + y² = 1 + d·x²·y² (mod p) where p = 2^255 - 19, d = -121665/121666

Signing:

1. r = H(private_key_prefix || message) // Deterministic! 2. R = r × G // Point multiplication 3. s = r + H(R || public_key || message) × private_key 4. Signature = (R, s)

Verification:

Check: s × G == R + H(R || public_key || message) × public_key

Why Ed25519

PropertyBenefit
DeterministicNo random number needed (no RNG failures)
Fast~70,000 signatures/second on modern CPU
Small64-byte signatures, 32-byte public keys
Secure128-bit security level
SimpleFewer implementation pitfalls than ECDSA

Signatures in Zentalk

LocationWhat’s SignedPurpose
Signed Pre-KeySPK + timestampProves SPK belongs to identity
Group messagesCiphertextAuthenticates sender to group
Key bundlesPublic keysPrevents key substitution

Kyber-768: Post-Quantum Cryptography

The Quantum Threat

Shor’s Algorithm can break:

  • RSA (factoring)
  • DH/ECDH (discrete logarithm)
  • ECDSA/Ed25519 (elliptic curve discrete log)

A sufficiently powerful quantum computer could decrypt all messages encrypted with these algorithms.

Why Kyber Is Quantum-Safe

Kyber is based on the Module Learning With Errors (M-LWE) problem:

Given: A (random matrix), b = A·s + e (where e is small error) Find: s (the secret)

Why this is hard:

  • No known quantum algorithm solves LWE efficiently
  • Best known quantum attack is still exponential
  • Even with error, recovering s is computationally infeasible

Encapsulation vs Encryption

Kyber is a Key Encapsulation Mechanism (KEM), not encryption:

Encryption:

ciphertext = Encrypt(public_key, message) message = Decrypt(private_key, ciphertext)

Encapsulation:

ciphertext, shared_secret = Encapsulate(public_key) shared_secret = Decapsulate(private_key, ciphertext)

The sender doesn’t choose the shared secret - they encapsulate a random value. This provides stronger security guarantees (IND-CCA2).

Hybrid Approach

Zentalk combines X25519 and Kyber:

SK_classical = X3DH(...) // X25519-based SK_kyber = Kyber.Decapsulate(ciphertext) SK_final = HKDF(SK_classical || SK_kyber, "ZentalkHybrid")

Why hybrid:

  • If X25519 is broken (quantum computer): Kyber protects
  • If Kyber has undiscovered flaw: X25519 protects
  • Security = stronger of the two

Kyber Parameters

ParameterKyber-768
Security levelNIST Level 3 (~192-bit classical)
Public key1,184 bytes
Private key2,400 bytes
Ciphertext1,088 bytes
Shared secret32 bytes

PBKDF2: Password-Based Key Derivation

The Problem

Users have weak passwords. A password like “password123” has only ~40 bits of entropy.

An attacker can try billions of passwords per second with specialized hardware.

How PBKDF2 Slows Attacks

key = PBKDF2(password, salt, iterations=600000)

How it works:

U1 = HMAC(password, salt || 0x00000001) U2 = HMAC(password, U1) U3 = HMAC(password, U2) ... U600000 = HMAC(password, U599999) Output = U1 XOR U2 XOR U3 XOR ... XOR U600000

With 600,000 iterations:

AttackerGuesses/secondTime for 1 billion passwords
Without PBKDF210 billion0.1 seconds
With PBKDF2~17,000~17 hours

Salt Prevents Precomputation

Without salt, attackers can build rainbow tables - precomputed password→key mappings.

With salt:

salt = random(32 bytes) key = PBKDF2(password, salt, 600000)

Each salt requires a completely new rainbow table. With 256-bit salts, precomputation is infeasible.

Usage in Zentalk

ApplicationIterationsPurpose
Key backup encryption600,000Protect exported keys
Local storageDevice-dependentProtect IndexedDB

Security Properties Summary

PropertyMechanismProtected Against
ConfidentialityAES-256-GCMEavesdroppers
IntegrityGCM tagMessage modification
AuthenticationEd25519 signaturesImpersonation
Forward SecrecyDouble RatchetFuture key compromise
Post-CompromiseDH RatchetPast key compromise
Quantum ResistanceKyber-768Future quantum computers
Anonymity3-hop relayTraffic analysis

Local Storage & Encryption

This section details how Zentalk protects data at rest on user devices. Local storage security is critical because even perfect transport encryption is meaningless if an attacker can simply read unencrypted data from disk.

Storage Architecture

Zentalk uses IndexedDB as the primary local storage mechanism across all web-based platforms. The database architecture is designed with security compartmentalization in mind.

Database Structure Overview:

DatabaseObject StoresPurposeEncryption Layer
zentalk_identityidentity_keys, prekeys, signed_prekeysLong-term cryptographic identityStorage Master Key
zentalk_sessionssessions, sender_keysActive session stateSession Storage Key
zentalk_messagesmessages, attachments, draftsMessage historyMessage Storage Key
zentalk_contactscontacts, groups, blockedContact metadataMetadata Storage Key

Object Store Schemas:

StoreKey PathIndexed FieldsEncrypted Fields
identity_keyskey_idcreated_at, key_typeprivate_key, public_key
sessionssession_idpeer_id, updated_atroot_key, chain_keys, pending_prekey
messagesmessage_idconversation_id, timestamp, statuscontent, attachments, metadata
contactscontact_iddisplay_name, last_seenidentity_key, verified_status

Storage Encryption Layers:

The encryption architecture uses a layered approach where each layer provides distinct protection:

User Password ┌─────────────────────────────────┐ │ PBKDF2 Key Derivation │ │ (600,000 iterations) │ └─────────────────────────────────┘ Storage Master Key (SMK) ├──► Identity Storage Key ──► Encrypts identity_keys store ├──► Session Storage Key ──► Encrypts sessions store ├──► Message Storage Key ──► Encrypts messages store └──► Metadata Storage Key ──► Encrypts contacts store

Local Encryption Key Derivation

The storage encryption key hierarchy begins with user authentication. A user-provided password (or biometric-derived secret) is transformed into cryptographic keys through a carefully designed derivation process.

PBKDF2 Parameters:

ParameterValueRationale
AlgorithmPBKDF2-HMAC-SHA256Widely audited, hardware-resistant
Iterations600,000OWASP 2024 recommendation
Salt length256 bitsPrevents rainbow table attacks
Output length256 bitsMatches AES-256 key size
Salt storageUnencrypted in localStorageRequired for derivation

Key Derivation Pseudocode:

FUNCTION derive_storage_master_key(password, salt): IF salt IS NULL: salt = secure_random(32 bytes) store_salt(salt) master_key = PBKDF2( password = password, salt = salt, iterations = 600000, key_length = 32, hash = SHA256 ) RETURN master_key

Key Hierarchy Derivation:

From the Storage Master Key, individual per-database keys are derived using HKDF with distinct info strings:

Derived KeyHKDF InfoPurpose
Identity Storage Key"zentalk-identity-storage-v1"Encrypt identity database
Session Storage Key"zentalk-session-storage-v1"Encrypt session database
Message Storage Key"zentalk-message-storage-v1"Encrypt message database
Metadata Storage Key"zentalk-metadata-storage-v1"Encrypt contact database
FUNCTION derive_storage_keys(master_key): identity_key = HKDF( ikm = master_key, salt = NULL, info = "zentalk-identity-storage-v1", length = 32 ) session_key = HKDF( ikm = master_key, salt = NULL, info = "zentalk-session-storage-v1", length = 32 ) // Similar for message_key and metadata_key RETURN {identity_key, session_key, message_key, metadata_key}

Why Separate Keys Per Database:

BenefitDescription
Compromise isolationLeaking one key doesn’t expose all data
Selective decryptionCan unlock only needed databases
Key rotation independenceCan rotate one key without affecting others
Audit granularityCan track access per data category

Message History Encryption

Messages are encrypted individually at rest, providing defense in depth beyond session encryption.

Per-Message Encryption:

Each message stored in IndexedDB is encrypted with a unique key derived from the Message Storage Key:

FieldEncryptionDerivation
contentAES-256-GCMHKDF(message_storage_key, message_id)
attachmentsAES-256-GCMHKDF(message_storage_key, attachment_id)
metadataAES-256-GCMHKDF(message_storage_key, message_id + “meta”)
FUNCTION encrypt_message_at_rest(message, message_storage_key): message_encryption_key = HKDF( ikm = message_storage_key, salt = message.id, info = "zentalk-message-encrypt", length = 32 ) nonce = secure_random(12 bytes) encrypted_content = AES_GCM_Encrypt( key = message_encryption_key, nonce = nonce, plaintext = message.content, aad = message.id || message.conversation_id ) STORE { id: message.id, conversation_id: message.conversation_id, timestamp: message.timestamp, // Plaintext for indexing status: message.status, // Plaintext for queries nonce: nonce, encrypted_content: encrypted_content } SECURE_WIPE(message_encryption_key)

Session Survival After Session Reset:

A critical design consideration is that message history must survive session resets. The storage encryption keys are completely independent from session keys:

Key TypeDerived FromLifetimePurpose
Session KeysX3DH + Double RatchetPer-conversation, rotatingEncrypt messages in transit
Storage KeysUser passwordUntil password changeEncrypt messages at rest
Session Keys (Transport): Storage Keys (At Rest): ┌────────────────────────┐ ┌────────────────────────┐ │ Root Key │ │ Storage Master Key │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ Chain Key │ ──► │ Message Storage Key │ │ │ │ (no │ │ │ │ ▼ │ link) │ ▼ │ │ Message Key ──decrypt─►│ │ Per-Message Key │ │ │ │ │ │ │ (deleted after use) │ │ ▼ │ └────────────────────────┘ │ Stored ciphertext │ └────────────────────────┘

Why This Separation Matters:

ScenarioSession KeysStorage KeysMessage History
Session reset by userRegeneratedUnchangedPreserved
Safety number changeRegeneratedUnchangedPreserved
Password changeUnchangedRegeneratedRe-encrypted
Device wipeDestroyedDestroyedLost

Key Storage Security

Long-term cryptographic keys require special handling to protect against extraction and misuse.

Identity Key Protection:

Protection LayerMechanismProtects Against
Storage encryptionAES-256-GCM with Identity Storage KeyDisk access
Memory protectionCleared after use, allocated in secure heapMemory dumps
Access controlRequires user authenticationUnauthorized app access
FUNCTION store_identity_key(identity_keypair, identity_storage_key): nonce = secure_random(12 bytes) serialized = serialize({ public_key: identity_keypair.public, private_key: identity_keypair.private, created_at: current_timestamp(), key_version: 1 }) encrypted = AES_GCM_Encrypt( key = identity_storage_key, nonce = nonce, plaintext = serialized, aad = "identity-key-storage" ) IndexedDB.put("identity_keys", { key_id: hash(identity_keypair.public), nonce: nonce, ciphertext: encrypted, created_at: current_timestamp() }) SECURE_WIPE(serialized) SECURE_WIPE(identity_keypair.private)

Session Key Storage:

Active session state contains sensitive cryptographic material:

ComponentSensitivityStorage Treatment
Root KeyCriticalEncrypted, wiped from memory after derivation
Chain Keys (sending/receiving)HighEncrypted, rotated frequently
Message KeysHighNever stored (derived on demand, deleted after use)
Pending PrekeyMediumEncrypted, deleted after X3DH completion
Ratchet Public KeysLowEncrypted (for consistency)

Secure Key Deletion:

Expired and used keys must be securely deleted to maintain forward secrecy guarantees:

FUNCTION secure_delete_session(session_id, session_storage_key): // Retrieve session for secure wiping session = decrypt_session(session_id, session_storage_key) // Overwrite sensitive fields with random data SECURE_WIPE(session.root_key) SECURE_WIPE(session.sending_chain_key) SECURE_WIPE(session.receiving_chain_key) FOR EACH skipped_key IN session.skipped_message_keys: SECURE_WIPE(skipped_key) // Delete from IndexedDB IndexedDB.delete("sessions", session_id) // Request garbage collection hint schedule_gc_hint()

Skipped Message Key Handling:

Out-of-order messages require temporary storage of skipped message keys:

PolicyValueRationale
Maximum skipped keys1,000 per chainBalance between flexibility and storage
Expiration time7 daysLimit exposure window
StorageEncrypted with session keyConsistent protection

Data at Rest Protection

Understanding what is encrypted versus what remains in plaintext is crucial for threat modeling.

Encryption Status by Data Type:

Data CategoryEncryptedPlaintextRationale
Message contentYes-Core privacy requirement
Message timestamps-YesRequired for efficient queries
Message status-YesUI display without decryption
Contact namesYes-Privacy protection
Contact IDs-YesRequired for indexing
Identity private keysYes-Critical security
Identity public keysYes-Consistency (not secret)
Session root keysYes-Critical security
Group membership-YesRequired for message routing
Settings/preferences-YesNon-sensitive configuration

Attack Scenario Analysis:

Attack ScenarioData ExposedMitigation
Device theft (locked)Nothing (screen lock protects access)Full-disk encryption + screen lock
Device theft (unlocked, app closed)IndexedDB ciphertexts, saltsStorage encryption requires password
Device theft (app open)Decrypted keys in memoryMemory protection, auto-lock timer
Forensic analysis of diskEncrypted blobs, metadataNo plaintext secrets on disk
Forensic analysis of memory dumpPotentially active keysSecure memory allocation, key wiping
Malicious app on deviceLimited by OS sandboxingStorage encryption, secure enclaves
Cloud backup extractionDepends on backup encryptionOptional encrypted backup, or exclude

Memory Protection Considerations:

TechniqueBrowser SupportEffectiveness
Zeroing after useAllMedium (GC may copy)
Typed arrays (no GC)AllHigh for key material
WebAssembly memoryModernHigh (isolated heap)
Secure enclaveLimited (see below)Very high
Short key lifetimesAllReduces exposure window
FUNCTION secure_wipe(buffer): IF buffer IS TypedArray: // Direct memory access - most secure in browser crypto.getRandomValues(buffer) // Overwrite with random buffer.fill(0) // Then zero ELSE IF buffer IS String: // Strings are immutable - cannot be securely wiped // Minimize string usage for secrets LOG_WARNING("Cannot securely wipe string - use TypedArray") // Note: JavaScript GC may have already copied the data // This is a fundamental browser limitation

Cross-Platform Considerations

Storage security varies significantly across platforms due to different OS capabilities and security APIs.

Browser Storage Limitations:

BrowserStorage MechanismSecurity Limitations
ChromeIndexedDB + LevelDBNo OS-level encryption by default
FirefoxIndexedDB + SQLiteSame origin policy only
SafariIndexedDB + SQLitePotentially more restrictive
AlllocalStorageNever use for secrets (synchronous, no encryption)

Browser-Specific Concerns:

ConcernDescriptionMitigation
No secure enclave accessWeb Crypto API lacks hardware key storageSoftware encryption + short lifetimes
GC unpredictabilityCannot guarantee memory wipingUse TypedArrays, minimize copies
Extension accessBrowser extensions may access storageContent Security Policy, integrity checks
Dev toolsSophisticated users can inspectDefense in depth, not obscurity

Mobile Secure Enclaves:

PlatformSecure Storage APIZentalk Usage
iOSKeychain Services + Secure EnclaveStore Storage Master Key
AndroidAndroid Keystore + StrongBoxStore Storage Master Key
BothBiometric authenticationGate key access to face/fingerprint
MOBILE INTEGRATION (iOS/Android native wrapper): FUNCTION initialize_secure_storage(): IF platform IS iOS: // Check for Secure Enclave availability IF SecureEnclave.isAvailable(): master_key_ref = SecureEnclave.generateKey( algorithm = AES256, access_control = biometric_or_passcode, synchronizable = FALSE // Don't sync to iCloud ) ELSE: master_key_ref = Keychain.generateKey(...) ELSE IF platform IS Android: IF StrongBox.isAvailable(): master_key_ref = AndroidKeystore.generateKey( algorithm = AES256, user_authentication_required = TRUE, invalidated_by_biometric_enrollment = TRUE ) ELSE: master_key_ref = AndroidKeystore.generateKey( // Software-backed fallback ) RETURN master_key_ref

Desktop Keychain Integration:

PlatformKeychain SystemIntegration Method
macOSKeychain ServicesStore master key with ACL
WindowsWindows Credential Manager / DPAPIProtect master key with user login
Linuxlibsecret / GNOME Keyring / KWalletStore master key per desktop environment

Desktop Integration Pseudocode:

DESKTOP INTEGRATION: FUNCTION store_master_key_in_keychain(master_key): IF platform IS macOS: Keychain.addItem( service = "com.zentalk.storage", account = user_id, data = master_key, access = when_unlocked_this_device_only ) ELSE IF platform IS Windows: // DPAPI ties encryption to user login encrypted = DPAPI.Protect( data = master_key, scope = current_user ) CredentialManager.write( target = "Zentalk Storage Key", credential = encrypted ) ELSE IF platform IS Linux: // Use secret service API (works with GNOME Keyring, KWallet) SecretService.store( schema = "com.zentalk.storage", attributes = {user_id: user_id}, secret = master_key, collection = default_collection )

Platform Comparison Matrix:

FeatureWeb BrowseriOS NativeAndroid NativeDesktop Electron
Secure enclaveNoYes (A7+)Yes (StrongBox)No
Hardware-backed keysNoYesYes (TEE)Limited (TPM)
Biometric auth for keysNoYesYesOS-dependent
Memory protectionLimitedGoodGoodLimited
Secure key deletionBest effortGuaranteedGuaranteedBest effort
Backup exclusionNo controlentitlementsandroid:allowBackupApp-specific

Cross-Platform Key Synchronization:

Zentalk does not automatically synchronize storage encryption keys across devices:

ApproachSecurityUsabilityZentalk Choice
No syncHighestEach device independentDefault
Encrypted backup to serverMediumUser controls recoveryOptional
Cloud keychain syncLowerSeamless multi-deviceNot used
MULTI-DEVICE STRATEGY: Device A (phone): Storage Master Key A ──► Encrypts local DB A └──► User exports encrypted backup (optional) Cloud storage (E2E encrypted with backup password) Device B (tablet): User imports backup Storage Master Key B ──► Decrypts imported data │ Re-encrypts for local storage └──► Independent key, independent local encryption

Last updated on