-
Notifications
You must be signed in to change notification settings - Fork 0
Encryption Flow
Yura Filatov edited this page Jun 27, 2026
·
1 revision
Once you have a contact's verified public key, you can encrypt any data for them. The flow differs depending on whether prekeys are available.
1. Retrieve contact's stored public key (decrypted from SwiftData)
2. If contact has QuantumKeyMaterial:
HKDF(ECDH_secret || ML-KEM_secrets, info: kHybridTransportKeyInfo) → sessionKey
Else:
HKDF(ECDH_secret, info: kTransportKeyInfo) → sessionKey
3. Encode SealedPayload { message, prekeyBatch: nil }
4. AES-GCM.seal(SealedPayload, using: sessionKey, authenticating: AAD)
5. Contact detects fallback → generates our prekeys → sends them back
1. Pop oldest prekey from contact's stored batch
2. generateEphemeralKeyPair() → throwaway key, never persisted
3. If contact has QuantumKeyMaterial:
HKDF(ECDH(ephPriv, prekey) || ML-KEM_secrets, info: kHybridFSTransportKeyInfo) → sessionKey
Else:
HKDF(ECDH(ephPriv, prekey), info: kTransportKeyInfo) → sessionKey
4. Encode SealedPayload { message, prekeyBatch: pendingBatch or nil }
5. AES-GCM.seal(SealedPayload, using: sessionKey, authenticating: AAD)
6. Ephemeral private key discarded; recipient deletes prekey from SE on decrypt
Decryption mirrors encryption: the recipient reconstructs the session key using their Secure Enclave prekey private key and the sender's ephemeral public key (plus ML-KEM secrets if available), verifies the GCM tag, decodes the payload, and deletes the prekey. The session key never existed in persistent storage on either side.