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.pnpm add @me2em/crypto
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.import { deriveKeyArgon2id, ARGON2_PROFILES } from '@me2em/crypto';
const result = await deriveKeyArgon2id(password, ARGON2_PROFILES.INTERACTIVE);
// result.key → 32 bytes, result.salt → for storage
Canonical Signal X3DH key exchange.
PreKeyBundle (SPK + OTK). Enforces OTK consumption (no zero-fallback).generateSignedPreKey, generateOneTimePreKeys, verifySignedPreKey.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.import { establishChannel, encryptChannelMessage, decryptChannelMessage } from '@me2em/crypto';
const channel = await establishChannel(sharedSecret, 'chat-42');
const msg = await encryptChannelMessage(channel, 0, plaintext);
const decrypted = await decryptChannelMessage(channel, msg);
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 | Sequence counter violation |
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.