Cryptographic mechanisms for the Me2eM protocol: E2EE channels, X3DH key exchange, purpose-based key derivation, and envelope patterns for offline recipients and media chunk encryption.
v0.1.0-alpha.1 — envelopes (key-wrapping, media chunks) + full documentation.
CryptoKey objects (see @me2em/react vault for the React integration). Raw key material lives only in module-private WeakMap internals and is wiped at rotation.Protocol terms are domain-neutral. The terms most relevant to this package:
| Term | Definition |
|---|---|
| Channel | A long-lived E2EE relationship derived from an X3DH shared secret. |
| Ephemeral material | TTL-bound rotating material: SPK/OTK, cache keys, session tokens. |
| PreKeyBundle | The public key set an initiator needs to start X3DH with an offline recipient. |
| Envelope | A key wrapped via X3DH for offline delivery — self-contained. |
| Epoch | Rotation counter for channel root keys. Messages from previous epochs are undecryptable. |
Full glossary (including Identity, Handle, Attestation, Session): see the root README.
pnpm add @me2em/crypto
💡 This package depends on
@me2em/core(workspace) andhash-wasm(Argon2id). It is headless-only — no React imports, works in both browser and Node.js ≥ 20. For React hooks and seed phrase UX use@me2em/react. All three packages are designed to work together — see the root README for the full package map.
Low-level cryptographic primitives built on WebCrypto (crypto.subtle).
info strings use me2em/crypto/v1/… domain separation.signRaw, verifyRaw) for transient key material.import { hkdfWithInfo, importAeadKey, encryptAead } from '@me2em/crypto';
const derived = await hkdfWithInfo(masterKey, salt, 'me2em/crypto/v1/session', 32);
const key = await importAeadKey(derived);
const { ciphertext, iv } = await encryptAead(key, plaintext);
Key derivation and password utilities.
INTERACTIVE, SENSITIVE). Includes salt generation and verification. Passwords are NFKC-normalized before hashing (consistent with @me2em/core canonicalization) — hashes produced before NFKC was introduced will not verify for non-canonical passwords; re-derive.generateSecurePassword uses an unbiased (rejection-sampled) Fisher-Yates shuffle; its distribution is verified by scripts/password-shuffle-bias.mjs (outside CI).import { deriveKeyArgon2id, generateArgon2Salt } from '@me2em/crypto';
const salt = generateArgon2Salt('INTERACTIVE');
const result = await deriveKeyArgon2id(password, salt, 'INTERACTIVE');
// result.key → 32 bytes, result.salt → store alongside the hash
Canonical Signal X3DH key exchange.
PreKeyBundle (SPK + OTK). Enforces OTK consumption (no zero-fallback).generateSignedPreKey, generateOneTimePreKeys, verifySignedPreKey.usedOneTimePreKeyId returned by initiateX3DH (and carried in WrappedKey) is base64url unpadded of the OTK public key — URL-safe, consistent with @me2em/core wire formats. unwrapKeyForMe does not decode it (it is caller-facing metadata).⚠️ OTK consumption is destructive.
completeX3DHandunwrapKeyForMewipeourOneTimePreKey.keyPair.privateKeyIN PLACE after the DH that uses it (session-bound ephemeral-key model). An OTK is single-use: reusing the wiped buffer derives a corrupted secret SILENTLY — no error is thrown. Pass a copy if retention is needed. The identity and SPK private keys are never touched.
import { initiateX3DH, completeX3DH, verifySignedPreKey } from '@me2em/crypto';
const bundle = await fetchPreKeyBundle(bobId);
verifySignedPreKey(bundle, bobIdentityPub);
const result = await initiateX3DH(aliceIdentity, bundle);
// transmit result.ephemeralPublicKey to Bob
Long-lived E2EE channels derived from X3DH shared secrets.
channelId, initial epoch: 0, and per-message key derivation. The returned object is frozen (readonly fields) and brand-validated.REPLAY_DETECTED), bounded out-of-order delivery is accepted, the window resets on rotate.INVALID_KEY on every further operation).import { establishChannel, encryptChannelMessage, decryptChannelMessage, destroyChannel } from '@me2em/crypto';
const channel = await establishChannel(sharedSecret, 'chat-42');
const msg = await encryptChannelMessage(channel, 0, plaintext);
const decrypted = await decryptChannelMessage(channel, msg);
destroyChannel(channel); // explicit teardown (logout, deletion, compromise response)
X3DH-based key wrapping for offline recipients and deterministic media chunk encryption.
(fileKey, recordingId, chunkIndex) via HKDF. Safe because each file gets a unique key.import { wrapKeyForRecipient, generateFileKey, encryptMediaChunk } from '@me2em/crypto';
// Wrap a group key for a member
const wrapped = await wrapKeyForRecipient({
keyBytes: groupKey,
recipientBundle: memberBundle,
myIdentity: ownerIdentity,
contextSalt: new TextEncoder().encode(groupId),
info: 'me2em/crypto/v1/group-key',
});
// Encrypt media chunks
const fileKey = generateFileKey();
const { encrypted, iv } = await encryptMediaChunk(fileKey, chunkData, recordingId, 0);
All errors are CryptoError instances with code and level:
| code | level | Meaning |
|---|---|---|
INVALID_LENGTH |
KEY | Wrong key/salt/IV/identity length |
INVALID_KEY |
KEY | Key material unusable |
INVALID_SALT |
KEY | Salt validation failure |
INVALID_IV |
CIPHER | IV length or format error |
INVALID_ARGUMENT |
KEY | Invalid function argument |
DERIVATION_FAILED |
KDF | HKDF/KDF failure |
ENCRYPTION_FAILED |
CIPHER | AES-GCM encrypt failure |
DECRYPTION_FAILED |
CIPHER | AES-GCM auth failure or corrupt data |
REPLAY_DETECTED |
CIPHER | Duplicate sequence within the replay window |
SIGNATURE_FAILED |
SIGNATURE | Ed25519 signing error |
VERIFICATION_FAILED |
SIGNATURE | SPK/signature verification failed |
UNSUPPORTED_VERSION |
FORMAT | Unknown envelope version |
UNSUPPORTED_ALGORITHM |
FORMAT | Unsupported cipher/curve |
import { CryptoError } from '@me2em/crypto';
try {
await unwrapKeyForMe(params);
} catch (e) {
if (e instanceof CryptoError) {
console.log(e.code, e.level); // e.g. 'DECRYPTION_FAILED' / 'CIPHER'
}
}
crypto.subtle) is the only allowed crypto API.@me2em/core (workspace), hash-wasm (Argon2id).See packages/core README for the core package.
Apache License 2.0 — see LICENSE.
© 2026 Me2eM Organization. Built for privacy, openness, and user sovereignty.