vollcrypt

Post-quantum cryptography workspace for messaging, files, WebAssembly, desktop, and database security.

View the Project on GitHub BeratVural/vollcrypt

Vollcrypt Messages

E2EE Message Encryption and Session Management module for Node.js, WebAssembly, and Rust

npm (node) npm (wasm)


This module contains the cryptographic primitives and session managers needed to build secure end-to-end encrypted (E2EE) messaging systems. It is compiled from a single Rust core to three targets: Node.js (native bindings), WebAssembly, and native Rust.

Table of Contents


Installation

Node.js (Server-side & Native Addon)

npm install @vollcrypt/messages-node

Prebuilt native binaries are provided for:

WebAssembly (Browser / React / Next.js)

npm install @vollcrypt/messages-wasm

Rust (Cargo)

In a Cargo workspace:

vollcrypt-core = { path = "../vollcrypt/src/core" }

Quick Start

Node.js — Generate Keys and Encrypt a Message

import {
  generateEd25519Keypair,
  encryptAesGcm,
  decryptAesGcm,
} from '@vollcrypt/messages-node';
import crypto from 'crypto';

// Identity keypair
const [identitySecret, identityPublic] = generateEd25519Keypair();

// Session key (in practice, derived via KEM handshake)
const sessionKey = crypto.randomBytes(32);
const plaintext  = Buffer.from('Hello, Vollcrypt');

// Encrypt
const ciphertext = encryptAesGcm(sessionKey, plaintext, null);

// Decrypt
const decrypted = decryptAesGcm(sessionKey, ciphertext, null);
console.log(decrypted.toString()); // Hello, Vollcrypt

WebAssembly — Browser

import init, {
  generateEd25519Keypair,
  encryptAesGcm,
  decryptAesGcm,
} from '@vollcrypt/messages-wasm';

await init();

const [identitySecret, identityPublic] = generateEd25519Keypair();
const sessionKey = crypto.getRandomValues(new Uint8Array(32));
const plaintext  = new TextEncoder().encode('Hello, Vollcrypt');

const ciphertext = encryptAesGcm(sessionKey, plaintext, null);
const decrypted  = decryptAesGcm(sessionKey, ciphertext, null);
console.log(new TextDecoder().decode(decrypted));

Architecture and Protocol Design

The Vollcrypt messages module provides the core cryptographic mechanisms to build end-to-end encrypted sessions between endpoints. The state machine enforces Perfect Forward Secrecy (PFS) through time-windowed ratchets, and Post-Compromise Security (PCS) through X25519 Diffie-Hellman ratcheting.

E2EE Lifecycle Sequence Diagram

sequenceDiagram
    autonumber
    actor Alice as Alice (Client)
    actor Bob as Bob (Client)
    participant Server as Directory Server / KTransparency

    Note over Bob: Generates Identity Keypair (Ed25519)<br/>Ratchet Keypair (X25519)<br/>PQ KEM Keypair (ML-KEM-768)
    Bob->>Server: Register and upload Bob's public keys
    Server->>Server: Append Bob's keys to signed Key Transparency Log

    Note over Alice: Generates Identity Keypair (Ed25519)
    Alice->>Server: Query Bob's public keys
    Server-->>Alice: Bob's (X25519_pub, MLKEM_pub) + Signature verification
    
    Note over Alice: authenticated_kem_encapsulate(<br/>Alice_X25519_sk, Bob_X25519_pub, Bob_MLKEM_pub, Alice_ID_sk<br/>) -> (ciphertext, shared_secret)
    Note over Alice: Derive SRK & WindowKey_n<br/>encrypt_aes_gcm(payload, window_key) -> ciphertext
    Note over Alice: pack_envelope() -> sealed_packet
    Alice->>Server: Deliver sealed_packet (recipient: Bob)
    Server->>Bob: Forward sealed_packet
    
    Note over Bob: unpack_envelope(sealed_packet)
    Note over Bob: authenticated_kem_decapsulate(<br/>Bob_X25519_sk, Alice_X25519_pub, Bob_MLKEM_dk, ciphertext, Alice_ID_pk<br/>) -> shared_secret
    Note over Bob: Verify Alice's signature on ciphertext
    Note over Bob: Derive Bob's WindowKey_n & Decrypt payload

API Reference

The APIs are exposed in both the Node.js native binding (@vollcrypt/messages-node) and the WebAssembly binding (@vollcrypt/messages-wasm).


1. Identity and Key Exchange

generateEd25519Keypair() → [secretKey: Buffer, publicKey: Buffer]

Generates a new Ed25519 keypair for user or device signing identity.

signMessage(secretKey: Uint8Array, message: Uint8Array) → Buffer

Signs a message with an Ed25519 private key.

verifySignature(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) → boolean

Verifies an Ed25519 digital signature.

generateX25519Keypair() → [secretKey: Buffer, publicKey: Buffer]

Generates a new X25519 Diffie-Hellman keypair for classical key exchange.

ecdhSharedSecret(ourSecret: Uint8Array, theirPublic: Uint8Array) → Buffer

Computes an X25519 ECDH shared secret.

generateMnemonic() → string

Generates a new random English BIP-39 mnemonic phrase containing 24 words (256-bit entropy).

mnemonicToSeed(phrase: string, password?: string | null) → Buffer

Converts a BIP-39 mnemonic phrase to a 64-byte binary seed.


2. Post-Quantum Cryptography (PQC)

mlKemKeygen() → [decapsKey: Buffer, encapsKey: Buffer]

Generates a new ML-KEM-768 keypair (NIST FIPS 203).

mlKemEncapsulate(encapsulationKey: Uint8Array) → MlKemEncapsulationResult

Performs ML-KEM-768 key encapsulation.

mlKemDecapsulate(decapsulationKey: Uint8Array, ciphertext: Uint8Array) → Buffer

Decapsulates a KEM ciphertext to extract the shared secret.

hybridKemEncapsulate(x25519OurSecret: Uint8Array, x25519TheirPublic: Uint8Array, mlKemEk: Uint8Array) → HybridKemResult

Performs a hybrid KEM encapsulation combining X25519 and ML-KEM-768.

hybridKemDecapsulate(x25519OurSecret: Uint8Array, x25519TheirPublic: Uint8Array, mlKemDk: Uint8Array, mlKemCt: Uint8Array) → Buffer

Decapsulates a hybrid KEM ciphertext.

authenticatedKemEncapsulate(ourX25519Sk: Uint8Array, recipientX25519Pub: Uint8Array, recipientMlkemEk: Uint8Array, senderIdentitySk: Uint8Array) → [ciphertext: Buffer, sharedSecret: Buffer]

Performs a hybrid KEM encapsulation and signs the ciphertext with the sender’s Ed25519 identity key.

authenticatedKemDecapsulate(ourX25519Sk: Uint8Array, senderX25519Pub: Uint8Array, ourMlkemDk: Uint8Array, authenticatedCiphertext: Uint8Array, senderIdentityPk: Uint8Array) → Buffer

Verifies the sender’s signature before decapsulating the hybrid shared secret.


3. Symmetric Encryption (AES-GCM)

All symmetric algorithms use AES-256-GCM. Nonces/IVs are 12 bytes and are generated internally using a cryptographically secure random number generator (OsRng).

encryptAesGcm(key: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array | null) → Buffer

Encrypts payload. The internally generated 12-byte IV is prepended to the returned ciphertext.

decryptAesGcm(key: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array | null) → Buffer

Decrypts and validates an AES-GCM ciphertext. Assumes a prepended 12-byte IV.

encryptAesGcmPadded(key: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array | null) → Buffer

Encrypts plaintext after padding it to standard boundaries (using PKCS#7 padding) to hide message size.

decryptAesGcmPadded(key: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array | null) → Buffer

Decrypts and removes PKCS#7 padding.

encryptAesGcmChunked(key: Uint8Array, plaintext: Uint8Array, aad: Uint8Array | null, chunkSize: number) → Buffer

Splits payload into multiple chunks of size chunkSize and encrypts each independently.

decryptAesGcmChunked(key: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array | null) → Buffer

Decrypts a chunked ciphertext stream.

padMessage(content: Uint8Array) → Buffer

Pads message data according to PKCS#7 standard block padding.


4. Key Derivation and Wrapping

derivePbkdf2(password: Uint8Array, salt: Uint8Array, iterations: number, keyLen: number) → Buffer

Derives a key from a password using PBKDF2-SHA256.

deriveHkdf(ikm: Uint8Array, salt: Uint8Array | null, info: Uint8Array | null, keyLen: number) → Buffer

Derives a cryptographically strong key from input keying material (IKM) using HKDF-SHA256.

deriveSrk(dek: Uint8Array, chatId: Uint8Array) → Buffer

Derives a 32-byte Session Root Key (SRK) from a Data Encryption Key (DEK) and a unique chat identifier.

deriveWindowKey(srk: Uint8Array, windowIndex: number) → Buffer

Derives a time-window-specific encryption key from the Session Root Key.

wrapKey(kek: Uint8Array, keyToWrap: Uint8Array) → Buffer

Wraps a key using AES-256 Key Wrap (AES-KW, RFC 3394) under a Key Encryption Key (KEK).

unwrapKey(kek: Uint8Array, wrappedKey: Uint8Array) → Buffer

Unwraps an AES-KW wrapped key.


5. Session Security & Ratcheting

generateRatchetKeypair() → [secretKey: Buffer, publicKey: Buffer]

Generates a new ephemeral X25519 keypair for Post-Compromise Security (PCS) ratcheting steps.

ratchetSrk(currentSrk: Uint8Array, ourRatchetSecret: Uint8Array, theirRatchetPub: Uint8Array, chatId: Uint8Array, ratchetStep: number, isSender: boolean) → Buffer

Advances the Session Root Key (SRK) with an ephemeral ECDH exchange value.

shouldRatchet(messageCount: number, windowChanged: boolean, messagesPerRatchet: number, ratchetOnNewWindow: boolean) → boolean

Utility logic to check if the client is due to rotate keys.


6. Transcript Hashing

Transcript hashing provides replay, deletion, and out-of-order message insertion detection by linking each message to the historical chain hash state.

transcriptNew(sessionId: Uint8Array) → Buffer

Initializes a new transcript chain state.

transcriptComputeMessageHash(messageId: Uint8Array, senderId: Uint8Array, timestamp: number, ciphertext: Uint8Array) → Buffer

Computes the SHA-256 hash of a message envelope.

transcriptUpdate(chainState: Uint8Array, messageHash: Uint8Array) → Buffer

Updates the running chain state.

transcriptVerifySync(hashA: Uint8Array, hashB: Uint8Array) → boolean

Verifies whether two transcript hashes match using a constant-time comparison.


7. Sealed Sender

sealMessage(recipientX25519Pub: Uint8Array, senderId: Uint8Array, content: Uint8Array) → Buffer

Encrypts the sender’s identity together with the content under an ephemeral ECDH key, hiding sender metadata from routing servers.

unsealMessage(sealedPacket: Uint8Array, ourX25519Sk: Uint8Array) → [senderId: Buffer, content: Buffer]

Decrypts a sealed sender packet.


8. Key Verification Codes

generateVerificationCode(keyA: Uint8Array, keyB: Uint8Array, conversationId: Uint8Array) → string

Generates a JSON string containing out-of-band verification codes (both formatted decimal groups and emoji sequences) for human comparison.


9. Key Transparency Log

keyLogCreateEntry(userId: Uint8Array, publicKey: Uint8Array, timestamp: number, prevEntryHash: Uint8Array, action: number, signingKey: Uint8Array) → string (json)

Generates and signs a Key Transparency append-only log entry.

keyLogVerifyChain(entriesJson: string) → boolean

Verifies the integrity, signature sequence, and linkage of a JSON array of Key Transparency log entries.


10. Device Registry

registryEmpty() → string (json)

Returns an empty device registry state object.

registryAddDevice(registryJson: string, deviceId: string, name: string, addedAt: number, publicKey: string) → string (json)

Adds a device public key to the registry JSON string.

registryRevokeDevice(registryJson: string, deviceId: string) → string (json)

Revokes a device by its ID.


Full E2EE Flow Example

This script illustrates the complete cryptographic pipeline for generating keys, performing the Hybrid KEM handshake, deriving keys, encrypting a message via Sealed Sender, updating transcripts, and decrypting the result.

import {
  generateEd25519Keypair,
  generateX25519Keypair,
  generateMlKem768Keypair,
  authenticatedKemEncapsulate,
  authenticatedKemDecapsulate,
  deriveSrk,
  deriveWindowKey,
  transcriptNew,
  transcriptComputeMessageHash,
  transcriptUpdate,
  transcriptVerifySync,
  encryptAesGcm,
  decryptAesGcm,
  sealMessage,
  unsealMessage
} from '@vollcrypt/messages-node';

// 1. Participant Identity Keypair Generation
const [aliceIdSk, aliceIdPk] = generateEd25519Keypair();
const [aliceX25519Sk, aliceX25519Pk] = generateX25519Keypair();
const [aliceMlkemDecaps, aliceMlkemEncaps] = generateMlKem768Keypair(); // [decaps, encaps]

const [bobIdSk, bobIdPk] = generateEd25519Keypair();
const [bobX25519Sk, bobX25519Pk] = generateX25519Keypair();
const [bobMlkemDecaps, bobMlkemEncaps] = generateMlKem768Keypair();

// 2. Hybrid Handshake Execution
const conversationId = Buffer.from('conv-alice-bob-001');

// Alice generates an ephemeral X25519 keypair for the hybrid KEM
const [aliceEphSk, aliceEphPk] = generateX25519Keypair();

// Alice encapsulates key material for Bob
const [authCiphertext, aliceSharedSecret] = authenticatedKemEncapsulate(
  aliceEphSk,      // Alice's ephemeral X25519 secret
  bobX25519Pk,     // Bob's static X25519 public key
  bobMlkemEncaps,  // Bob's static ML-KEM encapsulation key
  aliceIdSk        // Alice's signing key (for authentication)
);

// Bob receives Alice's ephemeral public key (extracted from handshake metadata or sent alongside)
// and decapsulates the shared secret
const bobSharedSecret = authenticatedKemDecapsulate(
  bobX25519Sk,      // Bob's static X25519 secret key
  aliceEphPk,       // Alice's ephemeral X25519 public key
  bobMlkemDecaps,   // Bob's static ML-KEM decapsulation key
  authCiphertext,   // The signed KEM ciphertext envelope
  aliceIdPk         // Alice's static Ed25519 public key
);

// 3. Key Derivation (SRK & Window Keys)
const aliceSrk = deriveSrk(aliceSharedSecret, conversationId);
const bobSrk   = deriveSrk(bobSharedSecret, conversationId);

const windowIndex = Math.floor(Date.now() / 1000 / 3600);
const aliceWindowKey = deriveWindowKey(aliceSrk, windowIndex);
const bobWindowKey   = deriveWindowKey(bobSrk, windowIndex);

// 4. Transcript Initialization
let aliceChain = transcriptNew(conversationId);
let bobChain   = transcriptNew(conversationId);

// 5. Encrypting & Sealing the Message (Alice)
const messageId = Buffer.from('msg-001');
const senderId  = Buffer.from('alice@example.com');
const timestamp = Math.floor(Date.now() / 1000);
const aad       = Buffer.concat([messageId, senderId, Buffer.from(timestamp.toString())]);
const plaintext = Buffer.from('Hello Bob');

const ciphertext = encryptAesGcm(aliceWindowKey, plaintext, aad);
const sealed     = sealMessage(bobX25519Pk, senderId, ciphertext);

// Update Alice's Transcript chain hash state
const msgHash = transcriptComputeMessageHash(messageId, senderId, timestamp, ciphertext);
aliceChain    = transcriptUpdate(aliceChain, msgHash);

// 6. Unsealing & Decrypting the Message (Bob)
const [revealedSender, revealedCiphertext] = unsealMessage(sealed, bobX25519Sk);
const decrypted = decryptAesGcm(bobWindowKey, revealedCiphertext, aad);

// Update Bob's Transcript chain hash state
bobChain = transcriptUpdate(bobChain, msgHash);

// 7. Verify Integrity and Equivalence
console.log(decrypted.toString());                       // "Hello Bob"
console.log(transcriptVerifySync(aliceChain, bobChain)); // true

Secure Client Integration Guidelines

When integrating @vollcrypt/messages-node or @vollcrypt/messages-wasm inside client applications, you must obey the following cryptographic safety mandates:

1. Active Memory Zeroization

JavaScript runtimes do not automatically clear memory and are vulnerable to garbage-collection delays, which leaves raw keys exposed in memory.

2. Leverage Non-Extractable CryptoKeys (Web Crypto API)

Never store raw public/private keys in plaintext inside Javascript context variables or state trees.

3. Avoid Persistent Insecure Storage


Test Coverage and Verification

Vollcrypt Messages is covered by a highly comprehensive suite of 219 unit, integration, and adversarial tests that are run in the Rust core module.

To execute the tests:

cargo test --manifest-path vollcrypt-messages/core/Cargo.toml

Verification Status

All tests compile warning-free and pass successfully:

Test Category Total Tests Passed Ignored Status
Identity and Key Exchange 18 18 0 ✅ Passed
Post-Quantum Cryptography (PQC) 14 14 0 ✅ Passed
Symmetric Encryption (AES-GCM) 35 35 0 ✅ Passed
Key Derivation and Wrapping 12 12 0 ✅ Passed
OOB Verification Codes 10 10 0 ✅ Passed
Key Transparency Log 8 8 0 ✅ Passed
Device Registry 5 5 0 ✅ Passed
Adversarial & Stress Integration Suites 8 8 0 ✅ Passed
Others 109 109 0 ✅ Passed
Total 219 219 0 ✅ All Tests Passed

This includes advanced stress tests covering network chaos and out-of-order recovery, concurrent PCS ratchet updates and transactional state rollbacks, sealed sender byte malleability (verifying immunity to bit-flips across all packet bytes), and unwinding panic memory zeroization checks.


Performance Benchmarks

Vollcrypt Messages includes a dedicated high-performance benchmarking suite in Rust to measure the raw execution speed and throughput of the cryptographic core.

Running the Benchmarks

To run the performance benchmarks:

cargo run --manifest-path vollcrypt-messages/core/Cargo.toml --release --bin perf

Note: For maximum performance on x86_64 CPUs, ensure you build with hardware AES-NI acceleration enabled:

RUSTFLAGS="-C target-cpu=native" cargo run --manifest-path vollcrypt-messages/core/Cargo.toml --release --bin perf

Automated Hardware & Resource Monitoring

We provide an automated PowerShell runner script (benchmark_runner.ps1) that compiles the benchmark binary under release mode with AES-NI acceleration, collects your hardware specifications, tracks real-time CPU/RAM/GPU/Disk utilization, and formats everything into a markdown report.

To run this automated script on Windows:

powershell -ExecutionPolicy Bypass -File vollcrypt-messages/core/benchmark_runner.ps1

The script will run the benchmarks, track resource usage, and generate a new performance_report_optimized.md inside vollcrypt-messages/core/.

Reference Benchmark Figures

The following values represent typical execution speeds measured under a Zero-Allocation architecture utilizing native hardware AES-NI acceleration on standard modern hardware (e.g. Intel Core i7 / AMD Ryzen 5 class CPU):

Symmetric Throughput (AES-256-GCM)

| Operation | Data Payload Size | Throughput (Pure Software) | Throughput (Hardware AES-NI) | | :— | :—: | :—: | :—: | | AES-GCM Encryption | 64 KB | ~300 MB/s | ~9,200 MB/s | | AES-GCM Decryption | 64 KB | ~295 MB/s | ~10,200 MB/s | | AES-GCM Encryption | 1 MB | ~290 MB/s | ~9,300 MB/s | | AES-GCM Decryption | 1 MB | ~300 MB/s | ~10,100 MB/s | | AES-GCM Chunked (1MB Chunks) | 16 MB | ~240 MB/s | ~5,200 MB/s |

Cryptographic Primitives & Handshakes

| Primitive | Operation / Scenario | Execution Speed (ops/second) | | :— | :—: | :—: | | HKDF-SHA256 | Key Expansion (32-byte key) | ~850,000 ops/s | | PBKDF2-HMAC-SHA256 | Password Hashing (600,000 iterations) | ~11.6 ops/s (Single thread) | | ML-KEM-768 | Encapsulation (Client Handshake) | ~8,300 ops/s | | ML-KEM-768 | Decapsulation (Recipient Handshake) | ~6,500 ops/s | | Sealed Sender | Ephemeral DH Seal (Alice) | ~16,600 ops/s | | Sealed Sender | Decryption & Parse Unseal (Bob) | ~22,400 ops/s | | PCS Ratchet | Ephemeral X25519 Key Pair Gen | ~64,600 ops/s | | PCS Ratchet | Session KDF Update (Step Computation) | ~21,800 ops/s | | Key Verification | Out-of-band Numeric/Emoji Code Gen | ~473,000 ops/s | | Key Transparency | 100-entry Chain Verification | ~300 ops/s |

Asymmetric Identity Primitives (Ed25519 & BIP-39)

| Primitive | Operation / Scenario | Execution Speed (ops/second) | | :— | :—: | :—: | | Ed25519 Signing | Sign 1KB Payload | ~28,400 ops/s | | Ed25519 Verification | Verify 1KB Signature | ~30,600 ops/s | | Ed25519 Signing | Sign 1MB Payload | ~410 ops/s | | Ed25519 Verification | Verify 1MB Signature | ~820 ops/s | | BIP-39 Mnemonic | 24-word Mnemonic to 64-byte Seed | ~1,090 ops/s |

Multi-threaded Concurrency Throughput

| Scenario | Execution Context | Aggregate Throughput | | :— | :— | :—: | | Multi-threaded Handshake Scaling | Concurrent ML-KEM Encaps + Unseal (12 threads) | ~88,200 ops/s |

Memory State Lookup Latencies (Replay Prevention Store)

| Operation | Target Hash State | Average Latency | | :— | :—: | :—: | | Hash Lookup (Hit) | Populated with 100,000 hashes | ~91.64 ns | | Hash Lookup (Miss) | Populated with 100,000 hashes | ~36.40 ns | | Hash Insertion | Populated with 100,000 hashes | ~109.78 ns |

Measured Performance on Test Device

The actual performance benchmarks measured on the test device, along with the device’s hardware specs and real-time resource utilization, are recorded in the Performance Report.

System Specifications

Executed Benchmark Metrics (with Hardware AES-NI & Zero-Allocation)

Resource Utilization during run

ON THIS PAGE