Post-quantum E2EE
for TON Web3
The first end-to-end encryption library for TON wallets that is resistant to quantum computers. ML-KEM-768 + P-256 X3DH + Double Ratchet. Rust WASM core. Zero npm dependencies.
npm install @stvor/web3
Why post-quantum?
Elliptic curve cryptography (Ed25519, secp256k1, P-256) used by every Web3 wallet today is vulnerable to Shor's algorithm running on a sufficiently large quantum computer.
@stvor/web3 uses ML-KEM-768 — standardised by NIST in FIPS 203 (August 2024) — combined with classical P-256 in a hybrid scheme. Breaking it requires breaking both simultaneously, which is impossible with any known algorithm.
Features
Crypto stack
Installation
npm install @stvor/web3
Works in any environment with Web Crypto API and WebAssembly — all modern browsers and Node.js ≥ 18.
Quick start
Initialize the WASM crypto engine
import initWasm from '@stvor/web3/wasm';
const wasm = await initWasm();
Connect with a TON wallet
import { StvorWeb3 } from '@stvor/web3';
const client = await StvorWeb3.connect({
provider: tonConnectProvider, // TonConnect wallet
contractAddress: 'EQD...', // stvor_registry address
tonApiUrl: 'https://testnet.toncenter.com/api/v2',
wasm,
});
Listen for messages
client.onMessage(msg => {
console.log(`From ${msg.from}:`, msg.data);
// msg.timestamp, msg.id also available
});
Send a post-quantum encrypted message
await client.send('0:bob_address...', {
text: 'Hello, quantum-safe Web3!',
// any JSON-serializable payload
});
TON Connect integration
@stvor/web3 uses a minimal TonConnectProvider interface — no external SDK dependency.
If you already use @tonconnect/sdk, wrap it with the adapter below.
import { TonConnect } from '@tonconnect/sdk';
const connector = new TonConnect();
await connector.connect({ universalLink: 'https://app.tonkeeper.com/ton-connect' });
// Adapter — maps TonConnect to TonConnectProvider interface
const provider = {
account: {
address: connector.account.address,
chain: connector.account.chain,
},
async signData(payload) {
return connector.signData(payload);
},
async signTransaction(to, bodyHex, value) {
const result = await connector.sendTransaction({
messages: [{ address: to, amount: value, payload: bodyHex }],
});
return result.boc;
},
};
const client = await StvorWeb3.connect({ provider, wasm, /* ... */ });
API reference
StvorWeb3.connect(config)
Creates and connects a client. Registers public keys on-chain via the registry contract.
| Parameter | Type | Description |
|---|---|---|
| provider | TonConnectProvider | TON wallet provider |
| contractAddress | string | stvor_registry address |
| tonApiUrl | string | TonCenter API endpoint |
| wasm | WasmModule | Initialized WASM module |
| tonApiKey? | string | Optional TonCenter API key |
| pollIntervalMs? | number | Poll interval in ms (default: 5000) |
| pqc? | boolean | Enable ML-KEM-768 hybrid (default: true) |
client.send(to, data)
Encrypts data with a hybrid post-quantum session and delivers it via TON Storage.
await client.send('0:abc...', { text: 'Hello!' });
client.onMessage(handler)
Registers a message handler. Returns an unsubscribe function.
const unsub = client.onMessage(msg => {
// msg.from — sender TON address
// msg.data — decrypted payload
// msg.timestamp — Date
// msg.id — message ID
});
unsub(); // stop listening
client.disconnect()
Stops message polling and clears all session state from memory.
Low-level WASM API
Direct access to the Rust crypto primitives for advanced use cases.
ML-KEM-768
import { wasm_mlkem_keygen, wasm_mlkem_encaps, wasm_mlkem_decaps }
from '@stvor/web3/wasm';
// Generate keypair — { ek: string, dk: string } (base64url)
const { ek, dk } = JSON.parse(wasm_mlkem_keygen());
// ek = 1184-byte encapsulation key (public)
// dk = 64-byte decapsulation key seed (secret)
// Encapsulate: generates a shared secret and a ciphertext
const { ct, ss } = JSON.parse(wasm_mlkem_encaps(ek));
// Decapsulate: recover the shared secret from ciphertext
const ss2 = wasm_mlkem_decaps(dk, ct);
// ss === ss2 ✓
Hybrid X3DH
import { WasmKeyPair, wasm_mlkem_keygen,
wasm_hybrid_session_initiate, wasm_hybrid_session_respond }
from '@stvor/web3/wasm';
const aliceIK = new WasmKeyPair();
const aliceSPK = new WasmKeyPair();
const bobIK = new WasmKeyPair();
const bobSPK = new WasmKeyPair();
const bobPqc = JSON.parse(wasm_mlkem_keygen());
// Alice: initiate hybrid session → get session + ML-KEM ciphertext
const { session_json, mlkem_ct } = JSON.parse(
wasm_hybrid_session_initiate(
aliceIK, aliceSPK,
bobIK.public_key, bobSPK.public_key,
bobPqc.ek,
)
);
const alice = WasmSession.from_json(session_json);
// Bob: respond with ML-KEM ciphertext from Alice
const bob = wasm_hybrid_session_respond(
bobIK, bobSPK,
aliceIK.public_key, aliceSPK.public_key,
bobPqc.dk, mlkem_ct,
);
Session encrypt / decrypt
// Encrypt
const blob = alice.encrypt(new TextEncoder().encode('gm'));
// Decrypt
const pt = bob.decrypt(blob);
new TextDecoder().decode(pt); // → 'gm'
// Serialize / restore session state
const json = alice.to_json();
const restored = WasmSession.from_json(json);
Security
Hybrid PQC key derivation
shared_key = HKDF-SHA256(
ikm = ecdh_sk ‖ mlkem_ss, // P-256 ‖ ML-KEM-768
salt = 0x00...00 (32 bytes),
info = "STVOR-HYBRID-v1"
)
shared_key requires breaking both P-256 ECDH and ML-KEM-768 simultaneously. No known classical or quantum algorithm can do this.
NIST ACVTS verification
All primitives tested against official NIST Cryptographic Algorithm Validation Program vectors:
Forward secrecy
The Double Ratchet protocol generates a fresh symmetric key for every message. Compromising a session key exposes only that single message — not past or future ones.
Key zeroization
All private key material is zeroed from memory after use via the Rust zeroize crate. Keys do not linger in memory after a session ends.
Comparison
| @stvor/web3 | XMTP | Waku | Signal (libsignal) | |
|---|---|---|---|---|
| ML-KEM-768 (PQC) | ✓ | ✗ | ✗ | ✗ |
| Account Abstraction (ERC-4337 + TON v5) | ✓ | ✗ | ✗ | ✗ |
| TON wallet identity | ✓ | ✗ | ✗ | ✗ |
| Double Ratchet | ✓ | ✓ | ✗ | ✓ |
| On-chain key registry | ✓ | ✗ | ✗ | ✗ |
| Zero npm dependencies | ✓ | ✗ | ✗ | ✗ |
| NIST ACVTS verified | ✓ | ✗ | ✗ | ✓ |
| Rust WASM crypto core | ✓ | ✗ | ✗ | ✓ |
Account Abstraction
@stvor/web3 supports any smart wallet — ERC-4337 on EVM and TON Wallet v5 extensions.
Identity is derived from the wallet address, not from a private key.
The same API works for EOA wallets, Safe, Coinbase Smart Wallet, Biconomy, ZeroDev, and TON v5.
personal_sign via EIP-1193. No dependency on any AA SDK.signData TON Connect v2. Supports wallet v5 extensions — binds E2EE session to an extension body.EVM — ERC-4337 (Safe, Coinbase, Biconomy, ZeroDev…)
import { StvorAA } from '@stvor/web3';
import initWasm from '@stvor/web3/wasm';
const wasm = await initWasm();
// Works with ANY EIP-1193 provider — EOA or smart wallet
const client = await StvorAA.connectEVM({
provider: window.ethereum, // MetaMask, Safe, Coinbase, Biconomy...
chainId: 1,
wasm,
});
// Send PQC-encrypted message to any EVM address
await client.send('0xrecipient...', { text: 'gm from AA wallet!' });
console.log(client.address); // AA wallet address
console.log(client.chainType); // 'evm'
TON — Wallet v5 extension
const client = await StvorAA.connectTON({
provider: tonConnectProvider, // TON Connect v2
wasm,
});
// Sign a TON v5 extension body — proves ownership
const ext = client.signTonExtension('deadbeef0102');
// ext.bodyHex, ext.identitySig, ext.walletAddress
// Verify a peer's extension signature
const ok = client.verifyTonExtension(ext, peerIdentityKey);
UserOperation binding (ERC-4337)
// Bind an E2EE session to a specific UserOperation
const binding = client.bindUserOp(
userOpHash, // keccak256(UserOperation) — hex, 32 bytes
sessionKey, // Double Ratchet root key — base64url, 32 bytes
);
// binding.userOpHash, binding.identitySig, binding.sessionCommitment
// Bundler / relayer verifies the binding on-chain
const valid = client.verifyUserOp(binding, peerIdentityKey, sessionKey);
// → true: this E2EE session belongs to the AA wallet that sent the UserOp
Identity derivation
// Signature → deterministic PQC identity keys (pure Rust WASM)
// STVOR-AA-EVM-v1:{chainId}:{address} → personal_sign → HKDF
// IK = HKDF(sig, msg, "IK", 32) → P-256 keypair
// SPK = HKDF(sig, msg, "SPK", 32) → P-256 keypair
// ML-KEM-768 keypair → hybrid post-quantum session key exchange
const id = JSON.parse(wasm_aa_derive(
'0xwallet', '1', 'evm', signatureB64
));
// id.ik, id.spk, id.spk_sig, id.mlkem_ek, id.address, id.chain_type
Smart contract
The stvor_registry FunC contract stores public keys and message bag IDs on-chain. No central server. No trust required.
Contract address (testnet)
EQD... # deploy with: node contracts/deploy.mjs
Internal messages (write)
| Op | Code | Body |
|---|---|---|
| register_keys | 0x1001 | op + query_id + ref(IK) + ref(SPK) + ref(SIG) + ref(ML-KEM EK) |
| store_message | 0x1002 | op + query_id + to_addr(267) + bag_id(256) |
| delete_message | 0x1003 | op + query_id + bag_id(256) |
Get methods (read)
| Method | Input | Returns |
|---|---|---|
| get_keys | address_int | (ik_cell, spk_cell, sig_cell, ts) |
| get_messages | address_int | dict(256 → MsgMeta) |
| get_message_count | address_int | int |
Deploy to testnet
# Get testnet TON from @testgiver_ton_bot
cd contracts
export STVOR_MNEMONIC="word1 word2 ... word24"
node deploy.mjs
# Output:
# Contract address: EQD...
# ✓ Deployed successfully!
Live demo
Try the cryptographic primitives directly in your browser. No installation needed — the WASM engine runs locally.
Contributing
# Run all 53 tests
cd crypto-core && cargo test # 28 Rust (incl. NIST vectors)
cd sdk && node --experimental-wasm-modules \
--import tsx/esm src/__tests__/wasm.test.ts # 17 WASM
cd contracts && node --import tsx/esm \
tests/registry.test.ts # 8 contract
# Rebuild WASM after Rust changes
cd crypto-core && wasm-pack build --target web --out-dir ../sdk/wasm