Essenger Protocol
End-to-end encryption with per-message ephemeral sender keys, post-quantum hybrid cryptography and honestly described limitations
1. Introduction
Essenger Protocol provides end-to-end encryption for private and group chats on Web/PWA and iOS beta; the macOS client is not included in v1.0. It combines hybrid key agreement (ephemeral X25519 + ML-KEM-768, FIPS 203) via HKDF-SHA256, per-message sender-side ephemeral keys, AES-256-GCM authenticated encryption, and HMAC-SHA256 sender authentication. Regular chats do not use Double Ratchet or X3DH — see the limitations section for details.
Essenger Protocol achieves the following security goals:
- Privacy — messages can only be read by the recipient; the server stores only the ciphertext.
- Integrity - any change in the ciphertext is detected through the AES-GCM and HMAC-SHA256 authentication tags.
- Per-message sender ephemeral key — each message derives its key from a fresh ephemeral X25519 pair on the sender's side, which is deleted after sending. Compromising the sender's long-term key therefore does not retroactively disclose messages already sent. This is not full forward secrecy because the recipient uses a static long-term key. Full forward secrecy and post-compromise security for regular 1:1 and group chats are not claimed — see the limitations section.
- Post-quantum stability - Hybrid Mode v3 combines the X25519 with the ML-KEM-768 to ensure messages remain protected from the “collect now, decrypt later” attack when a quantum computer emerges that can break elliptic curve cryptography.
Encryption is required. There is no plaintext transmission mode.
2. Threat model
What are we protecting from?
- Passive interceptor — an attacker monitoring all network traffic between clients and the server cannot read the contents of messages.
- Compromised server — the server stores only ciphertexts, public keys and encrypted blobs of private keys. Complete server compromise does not reveal the plaintext of messages or private keys.
- Future quantum computer - Hybrid messages v3/v4 combines the classic X25519 with the ML-KEM-768. An attacker who writes ciphertext today and later obtains a cryptographically valid quantum computer will not be able to decrypt hybrid-encrypted messages.
- Impersonation when a key is compromised — sender authentication via HMAC-SHA256 using a static shared secret ECDH prevents identity spoofing even if the server is compromised.
What we don't protect against
- Device compromise — if an attacker gains full control of the device (root access, malware), he can read decrypted messages from memory or local storage.
- Metadata — the server sees the account of the sender and recipient on the current active path, delivery time, message size and network metadata. Server-blind Sealed Sender is in redesign and is not declared as a shipped function.
- Bypass user authentication — the protocol does not protect against social engineering or account interception at the application level. TOTP 2FA and TOFU mitigate but do not eliminate this risk.
3. Cryptographic primitives
| Primitive | Algorithm | Link | Application |
|---|---|---|---|
| Key agreement | X25519 | RFC 7748 | Ephemeral sender ECDH, account-level encryption, sender authentication |
| Authenticated encryption | AES-256-GCM | NIST SP 800-38D | Message encryption, key wrapping, storage encryption |
| Message Authentication | HMAC-SHA256 | RFC 2104 | Sender authentication, storage MAC |
| Key output | HKDF-SHA256 | RFC 5869 | Message key derivation, hybrid secret combination, recovery key |
| Post-quantum KEM | ML-KEM-768 | FIPS 203 | Hybrid key encapsulation in v3/v4 messages |
| KDF for password (primary) | Argon2id | RFC 9106 | Private key encryption (64 MB, t=3, p=1) |
| KDF for password (spare) | PBKDF2-SHA256 | RFC 8018 | Legacy private key encryption (600K iterations) |
Implementation libraries
- Web client:
@noble/curves(X25519),@noble/hashes(HKDF, HMAC, SHA-256),@noble/post-quantum(ML-KEM-768), Web Crypto API (AES-GCM),argon2-browser(Argon2id) - iOS/macOS client: Apple CryptoKit (
Curve25519,AES.GCM,HKDF<SHA256>,HMAC<SHA256>)
4. Key Agreement (Ephemeral X25519)
Essenger does not use X3DH or prekey bundles. Each device, upon registration, generates one long-term X25519 key pair and (for the post-quantum path) an ML-KEM-768 key pair. Public keys are uploaded to the server; private keys never leave the device.
- Long term key X25519 — a pair of keys generated on the device during registration. The public key is published on the server; Incoming messages are addressed to it.
- Key ML-KEM-768 - post-quantum key pair for hybrid path v3 (web only by default). The public key is published on the server.
- TOFU by UUID — the interlocutor’s public key is assigned (trust on first use) to the unchanged User UUID upon first contact.
Stream per message
Key reconciliation is performed again for everyone messages. The sender generates a fresh ephemeral X25519 pair, performs ECDH with the recipient's published static key, outputs the message key via HKDF-SHA256, and destroys the ephemeral private key:
KDF Specification (DM v2)
function messageKeyV2(ephPriv, recipientPub):
shared = X25519(ephPriv, recipientPub)
salt = ephPub
info = "essenger-e2e-v2"
return HKDF-SHA256(shared, salt, info, outputLen=32)
There are no signed pre-keys, one-time pre-keys, Ed25519 signature verification, or setting up a long session. Each message is self-contained: it carries the sender's own ephemeral public key, and the recipient deduces the message key directly from his static private key. The hybrid post-quantum variant (v3) adds the ML-KEM-768 to this - see section 6.
5. Per-message sender keys
In regular chats, Essenger does not implement Double Ratchet. There is no root key, chain key, ratchet session, or evolving state between messages. Every message is independent: it uses a fresh ephemeral X25519 pair on the sender's side (section 4) and is encrypted with its own one-time key. Full Double Ratchet — with forward secrecy and post-compromise security — is available in secret chats (section 13).
Message encryption
function encrypt(plaintext, recipientPub, senderStaticPriv):
// Fresh ephemeral pair for this message
ephPriv = random(32)
ephPub = X25519.publicKey(ephPriv)
shared = X25519(ephPriv, recipientPub)
msgKey = HKDF-SHA256(shared, salt=ephPub, info="essenger-e2e-v2", len=32)
blob = 0x02 || ephPub(32) || nonce(12) || AES-256-GCM(msgKey, plaintext)
senderAuth = HMAC-SHA256(blob, key=staticECDH(senderStaticPriv, recipientPub))
// ephPriv is reset immediately after use
return { ct: blob, sa: senderAuth }
Combined AES-GCM format
All AES-256-GCM ciphertext uses a combination format compatible with Apple CryptoKit:
What it gives - and what it doesn't give
The following applies to regular chats. For full forward secrecy, open a secret chat — it runs on Double Ratchet (section 13). In a regular chat, only the sender key is ephemeral. The message is addressed to the recipient's static long-term X25519 key; there is no key rotation or ratcheting. This means:
- Compromise long-term sender key does not retroactively disclose messages it has already sent (the sender's ephemeral keys have already been destroyed).
- Compromise recipient's long-term private key reveals both past and future messages addressed to him.
- Post-compromise security (recovery after hacking) is not provided. Signal-like Ratchet is included in the post-1.0 release.
6. Quantum Shield (ML-KEM-768, v3/v4)
Account-level encryption supports a hybrid post-quantum mode that combines X25519 ECDH with ML-KEM-768 (Modular Lattice Key Encapsulation Mechanism, FIPS 203). This provides protection against a “collect now, decrypt later” attack: even if all the ciphertext is written today, a future quantum computer will not be able to calculate the message keys.
Message format versions
| Version | Byte prefix | Description |
|---|---|---|
| v1 | 0x01 (implicit) | Static X25519 ECDH. Outdated. Saved only for compatibility with older posts. |
| v2 | 0x02 | Ephemeral X25519 ECDH. Per-message ephemeral key of the sender. Current fallback when the recipient does not have a PQ key. |
| v3 | 0x03 | Hybrid ML-KEM-768 + ephemeral X25519. Active post-quantum path (web→web by default). Fixed PQ ciphertext size (1088 bytes). |
| v4 | 0x04 | Reserved extensible hybrid markup with 2-byte PQ ciphertext length prefix. Not activated (no call-site) and is not sent. |
Wire format v2 (current default)
// Output key v2
ephPriv = random(32)
ephPub = X25519.publicKey(ephPriv)
shared = X25519(ephPriv, recipientPub)
messageKey = HKDF-SHA256(shared, salt=ephPub, info="essenger-e2e-v2", len=32)
// ephPriv is reset immediately after use
Wire format v3 (active hybrid path)
Combining hybrid keys
// Sender (encryption)
x25519Shared = X25519(ephPriv, recipientX25519Pub)
(pqCipherText, pqShared) = ML-KEM-768.Encapsulate(recipientPQPub)
combinedSecret = x25519Shared || pqShared
messageKey = HKDF-SHA256(combinedSecret, salt=ephPub, info="essenger-hybrid-v1", len=32)
// Recipient (decryption)
x25519Shared = X25519(myPriv, ephPub)
pqShared = ML-KEM-768.Decapsulate(pqCipherText, myPQPriv)
combinedSecret = x25519Shared || pqShared
messageKey = HKDF-SHA256(combinedSecret, salt=ephPub, info="essenger-hybrid-v1", len=32)
The hybrid combination is safe as long as at least one of the two algorithms has not been hacked. If X25519 is hacked by a quantum computer, but ML-KEM-768 remains secure, the combination key is still secure, and vice versa. Concatenation followed by HKDF is a safe combinator in the standard model.
Reserved markup v4
v4 is identical to v3 but adds a 2-byte length prefix pqCTLen for future extensibility to larger KEM parameters. This markup is not yet activated (no call-site) and is not sent.
Backward Compatibility
When decrypting, the first byte of the ciphertext is checked to determine the version:
0x03 for hybrid v3, 0x02 for ephemeral X25519 v2. If none match, the message is processed as v1 (legacy static ECDH). New messages are sent as v3 (when the recipient has a PQ key - web→web by default) or as v2 (when there is no PQ key).
7. Sender Authentication
Each account-level message contains the sender's authentication HMAC, which proves that the sender owns the private key corresponding to the claimed public key, without server involvement.
Calculation
function computeSenderAuth(ciphertextBytes, senderPriv, recipientPub):
staticShared = X25519(senderPriv, recipientPub)
authKey = HKDF-SHA256(staticShared, salt=empty, info="essenger-sender-auth", len=32)
mac = HMAC-SHA256(authKey, ciphertextBytes)
// Reset staticShared and authKey immediately
return mac // 32 bytes
Check
function verifySenderAuth(ciphertextBytes, expectedMac, senderPub, myPriv):
staticShared = X25519(myPriv, senderPub)
authKey = HKDF-SHA256(staticShared, salt=empty, info="essenger-sender-auth", len=32)
computed = HMAC-SHA256(authKey, ciphertextBytes)
return constantTimeEqual(computed, expectedMac)
Envelope format
The authenticated ciphertext is sent as a JSON envelope:
{
"ct": " ",
"sa": " "
}
Cross-platform compatibility
The JSON wrapper ensures that the web client (@noble/curves), and iOS client (CryptoKit) can parse the same message format. The base shared secret X25519 is deterministic in all implementations.
Sender authentication is only verified if the sender's public key is available in the local TOFU cache. For unknown senders, the check is skipped (soft degradation). Once TOFU remembers the key, all subsequent messages are authenticated.
8. Sealed Sender
The current shipping implementation hides the sender's identity inside the E2E-encrypted payload for the recipient client, but delivery on the active path occurs via an authenticated WebSocket. Therefore, the server still knows the sender's account for routing purposes. Completely server-blind Sealed Sender - a separate redesign in the roadmap, and not the current shipped claim.
Format
At the current client level, the plaintext is wrapped in a JSON envelope before encryption:
{
"_ss": true, // sealed sender flag
"sid": " ",
"sun": " ",
"msg": " "
}
This JSON is encrypted with the recipient's public key using the standard v2/v3/v4 path. The recipient decrypts, discovers _ss: true and extracts the sender's displayed identity from the load. This does not make the current transport server-blind.
Unpacking logic
function unwrapSealedSender(plaintext):
if plaintext starts with '{':
parsed = JSON.parse(plaintext)
if parsed._ss === true && typeof parsed.msg === 'string':
return { text: parsed.msg, senderId: parsed.sid, senderUsername: parsed.sun }
return plaintext // not sealed sender, return as is
On the current active path, the server sees the sender, recipient, time of sending, message size, and network metadata. Server-blind delivery requires a separate access-key/sealed-inbox design and is not declared as a shipped function.
9. Group E2E v2
Group encryption uses a fresh random message key and a sender-side ephemeral ECDH pair for every message, wrapping that message key separately for each participant. This provides confidentiality and message independence: compromising one message key does not expose the others. However, the message key is wrapped under each participant's long-term public key. As in regular private chats, there is no Double Ratchet: group messages do not provide forward secrecy. Compromising a participant's long-term private key exposes both past and future messages addressed to that participant. Group-key rotation and forward secrecy for groups remain on the roadmap. Groups currently use X25519 without ML-KEM, so there is no PQ layer.
Encryption stream
- Generating a random 32-byte
messageKey. - Generating an ephemeral key pair X25519
(ephPriv, ephPub). - We encrypt the plaintext with
messageKeyvia AES-256-GCM (random 12-byte IV). - For each group member:
- We calculate:
shared = X25519(ephPriv, memberPub) - We display the wrapping key:
wrappingKey = HKDF-SHA256(shared, salt=ephPub, info="essenger-group-e2e-v2", len=32) - Wrap:
wrappedMK = AES-256-GCM(wrappingKey, messageKey, randomIV) - Save as:
base64(mkIV(12) || wrappedMK+tag) - Reset the wrap key immediately.
- We calculate:
- Reset to zero
ephPriv(the ephemeral private key never leaves memory).
Wire format
{
"v": 2,
"eph": " ",
"iv": " ",
"ct": " ",
"mk": {
"<username1>": "<base64 mkIV(12) || wrappedMK+tag>",
"<username2>": "<base64 mkIV(12) || wrappedMK+tag>",
...
}
}
Decryption flow
- Parse the JSON payload and verify
v === 2. - Looking for your username in the dictionary
mk. If missing, the message is not intended for this user. - Derive the shared secret with
shared = X25519(myPriv, ephPub), then run HKDF as above. - Expand: divide
mkIV(12) || wrappedMK+tag, decrypt with the wrapping key. - Decrypt the ciphertext expanded
messageKey.
Ephemeral sender key in groups
The ephemeral private key is generated anew for each group message and destroyed immediately after all wrapping keys are calculated. This protects the sender's long-term key: compromising it does not retroactively expose messages already sent. This is not forward secrecy for recipients: the message key is wrapped under each participant's static long-term key, so compromising a participant's key exposes every message addressed to that participant, past and future. Full forward secrecy for groups is not claimed and remains on the roadmap.
Adding or removing participants
- Addition: new members are included in the
mkdictionary for subsequent messages. They cannot decrypt messages sent before they were added because they have no access to the earlier ephemeral keys. - Removal: Removed members are excluded from the dictionary
mk. Because each message uses a fresh ephemeral key, the remote participant cannot compute future wrapping keys.
Legacy v1 format
The v1 protocol for groups used static sender keys without wrapping them for each participant. Saved for backward compatibility only (decryptGroupMessageV1 delegates to accountE2E.decrypt). Auto-detection checks first groupCiphertextV2, rolling back to ciphertext for v1.
10. Key management
Trust On First Use (TOFU)
Long-term public keys are verified through TOFU by unchanged User UUID. During the first contact, the interlocutor’s public key is stored locally under the key tofu_{uuid}. During subsequent contacts, the stored key is compared with the received one. If they are different, the contact is rejected with an error code IDENTITY_KEY_CHANGED.
// TOFU check (based on unchanged UUID)
storedKey = load("tofu_{uuid}")
if storedKey exists and storedKey !== remotePub:
throw "The public key has changed - a MITM attack is possible"
else if storedKey absent:
save("tofu_{uuid}", remotePub)
Safety Numbers
Users can manually verify each other by comparing security numbers. The security number is calculated by lexicographically sorting both parties' public keys, concatenating them, SHA-256 hashing, and formatting them into 12 groups of 5 digits:
function generateSafetyNumber(myPubB64, theirPubB64):
sorted = [myPubB64, theirPubB64].sort()
combined = sorted[0] + sorted[1]
hash = SHA-256(combined)
digits = ""
for i in 0..29:
digits += String(hash[i] % 100).padStart(2, '0')
//Format as 12 groups of 5 digits
return groups of 5, space-separated // e.g. "04821 73920..."
Key transparency audit log
The server maintains an append-only table key_audit_log, which records all events associated with the keys. Each entry contains:
| Field | Type | Description |
|---|---|---|
user_id | UUID | User whose key has changed |
action | string | Event type (register, rotate, revoke) |
public_key_hash | string | SHA-256 hash of the new public key |
device_info | string | Name/type of device that initiated the change |
ip_address | string | Request source IP address |
created_at | datetime | Timestamp |
The audit log is write-protected by PostgreSQL policies to prevent retroactive modification.
Key rotation
- Long-term account key can be regenerated (new pair X25519 and, if necessary, ML-KEM-768). The event is recorded in
key_audit_logwith actionrotate/revoke. - TOFU-rebinding: when the peer's published key changes, the pinned value (by UUID) changes, and the client signals this to the user. There are no prekeys or periodic rotation of session keys - each message uses the sender's fresh ephemeral key.
Private key storage
Private keys are stored locally on the device (device-local mode). At rest, the private keys in localStorage are encrypted with a storage encryption key (SEK), which exists only in sessionStorage (cleared when closing the tab):
Format: "sek1:" + base64(IV(12) || AES-GCM(SEK, privateKey))
SEK = random 32 bytes, stored only in sessionStorage
For keys encrypted on the server, the private key is encrypted with the user's password:
- Argon2id (main):
0xA2 || salt(16) || IV(12) || AES-GCM(derived, privKey). Parameters: memory 64 MB, time cost 3, parallelism 1, hash length 32 bytes. - PBKDF2 (spare):
salt(16) || IV(12) || AES-GCM(derived, privKey). 600K SHA-256 iterations. Used when Argon2 WASM is not available. Legacy keys with 100K iterations are automatically migrated.
11. Recovery
Account recovery uses the BIP39 mnemonic phrase (12 words, 128 bits of entropy) as a human-readable backup of the cryptographic identity.
Generating mnemonics
phrase = BIP39.generateMnemonic(wordlist=english, entropy=128)
// Generates 12 English words, e.g. "abandon ability able about above absent..."
Output of the recovery key
seed = BIP39.mnemonicToSeed(phrase) // 64 bytes
recoveryKey = HKDF-SHA256(
ikm = seed,
salt = "essenger-recovery-v1",
info = "essenger-recovery-v1",
len = 32
)
// recoveryKey: Uint8Array(32)
Encrypted backup format (RP1)
The private key is encrypted with the recovery key and loaded as a backup:
// Encryption
salt = random(16)
aesKey = HKDF-SHA256(recoveryKey, salt, info="essenger-e2e-privkey-v1", len=32)
iv = random(12)
blob = "RP1" || salt || iv || AES-GCM(aesKey, privateKey, iv)
output = base64(blob)
Recovery flow
- The user enters a 12-word mnemonic phrase.
- The phrase is validated against the BIP39 dictionary (with normalization: lower case, numbering removed, spaces collapsed).
- The recovery key is output via HKDF.
- The RP1 blob is decrypted. The resulting private key is verified against the expected public key.
- If successful, the private key is loaded into memory and session storage.
Storing mnemonics
The mnemonic phrase is optionally cached in sessionStorage for the duration of the current session, encrypted per-session with the AES-256-GCM key, which exists only in memory. Mnemonics are never written to localStorage and is not sent to the server.
12. Security issues
Constant time comparison
All MAC checking uses constant-time comparison to prevent timing attacks:
function equal(a, b):
if a.length !== b.length: return false
diff = 0
for i in 0..a.length:
diff |= a[i] ^ b[i]
return diff === 0
Resetting key material
Ephemeral private keys, shared secrets and deduced key material are reset to zero (.fill(0)) immediately after use, usually in a block finally. This minimizes the window during which sensitive material exists in memory.
Known limitations
- There is no Double Ratchet in regular chats: there is no complete forward secrecy and post-compromise security. Only the sender's key is ephemeral; the recipient uses a static long-term key, so compromising his private key reveals past and future messages addressed to him. For complete forward secrecy there is secret chats on Double Ratchet (section 13); Ratchet group chats are not yet used.
- No deniability: Sender Authentication HMAC uses a static ECDH key, creating cryptographic proof that a specific sender originated the message.
- Group metadata: dictionary
mkin group v2 messages, the composition of the group is revealed to any observer who can see the ciphertext (even though the contents of the message are encrypted). - JavaScript Memory Model:
.fill(0)onUint8Array- this is the best-effort in runtimes with a garbage collector. The JavaScript engine can keep copies of sensitive data in memory. - TOFU without PKI: The initial key exchange is trusted on first use (by an unchanged User UUID). An active MITM attack at the moment of first contact can replace the keys. Security numbers allow manual verification, but do not force it.
- No external audit: The protocol is open source but not verified by an independent third party. Status - experimental.
13. Secret chats (Double Ratchet)
A secret chat is a separate, manually enabled conversation alongside the regular one. Unlike regular chats (section 5), it runs a full Double Ratchet on top of an X3DH handshake and provides forward secrecy and post-compromise security. The same transport also carries view-once messages. The iOS and web implementations mirror each other byte for byte and are covered by frozen test vectors (dr_handshake_v6). Secret chats are 1:1; no group ratchet is claimed.
Establishing a session - X3DH
Each device has a separate DR identity (stored in Keychain under its own service, regardless of the key of regular chats; only a numeric one shared with the account deviceID):
- Identity key — X25519 (Curve25519) agreement key binding the transcript.
- Signing key - Ed25519; signed pre-key (SPK) is signed with it.
- Signed pre-key + pool one-time pre-keys (OTPK) - published to the server; OTPK is consumed upon the first incoming handshake.
The initiator performs X3DH against the published set of the recipient, checking Ed25519-SPK signature (incorrect signature → invalidSignedPreKeySignature, MITM sign). From the X3DH Diffie-Hellman set, the root key is derived with which the ratchet is seeded:
// Initiator → recipient, first contact
DH1 = X25519(IK_a, SPK_b)
DH2 = X25519(EK_a, IK_b)
DH3 = X25519(EK_a, SPK_b)
DH4 = X25519(EK_a, OTPK_b) // if OTPK is available
rootKey = HKDF-SHA256(DH1 ‖ DH2 ‖ DH3 ‖ DH4)
Ratchet
State then advances through the classic Double Ratchet: a DH ratchet (a new X25519 pair when direction changes) over a symmetric-key ratchet for root and chain keys. Every message gets its own key derived from the current chain key, and sent message keys are immediately deleted. This provides both forward secrecy (compromising the current state does not expose past messages) and post-compromise security (after one clean exchange, the parties recover secrecy). Each session belongs to a username:deviceID pair
and serialized so that parallel writes do not damage the state of the ratchet (Ns/CKs/DHs/PN).
Wire format (frame 0x06)
Live secret chat messages follow a self-describing frame 0x06:
senderDeviceID - routing only (which session): the payload is authenticated by the Ratchet itself (AAD), so replacing the id does not give anything. The INITIAL flag marks the first (prekey) frame carrying the ephemeral public key of the initiator.
Trust and protection
- Per-device TOFU: the remote device identity key is fixed upon first contact; shift →
identityKeyChanged(MITM sign), the session is not established silently. - Fail-closed: if a DR session cannot be established to the recipient's device (no published set), secret/one-time message not sent in clear text - it is not sent at all.
- Anti-DoS: no more than 5 prekey messages per minute per sender.
- Secret chats are 1:1; no group Double Ratchet is claimed.
- Initial trust - TOFU without PKI: active MITM exactly at the moment of the first contact is possible; Safety numbers allow manual verification.
- The protocol is open and covered with test vectors, but an independent external audit has not yet been carried out. Status - experimental.
14. Links
- 1A. Langley, M. Hamburg, S. Turner. "Elliptic Curves for Security." RFC 7748, January 2016.
- 2H. Krawczyk, P. Eronen. "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)." RFC 5869, May 2010.
- 3H. Krawczyk, M. Bellare, R. Canetti. "HMAC: Keyed-Hashing for Message Authentication." RFC 2104, February 1997.
- 4M. Dworkin. "Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM)." NIST SP 800-38D, November 2007.
- 5NIST. "Module-Lattice-Based Key-Encapsulation Mechanism Standard." FIPS 203, August 2024.
- 6A. Biryukov, D. Dinu, D. Khovratovich. "Argon2: the memory-hard function for password hashing and other applications." RFC 9106, September 2021.
- 7K. Moriarty, et al. "PKCS #5: Password-Based Cryptography Specification Version 2.1." RFC 8018, January 2017.
- 8P. Palatinus, et al. "Mnemonic code for generating deterministic keys." BIP-0039, 2013.
- 9P. Schwabe, et al. "CRYSTALS-KYBER Algorithm Specifications and Supporting Documentation." NIST PQC Round 3 submission, 2022.