Skip to main content

Examples

RSA end-to-end: generate, persist, and restore a key pair

This pattern generates a key pair once, stores it in localStorage, and restores it on every subsequent page load — so the user only generates once.

import { useKeyPair, useEncrypt, useDecrypt } from 'react-e2ee';
import { useEffect, useState } from 'react';

const STORAGE_KEY = 'myapp:rsa-keypair';

export function PersistentRsaDemo() {
const { keyPair, serialized, generating, generate, importKeyPair, error } = useKeyPair();
const { encrypt } = useEncrypt({ publicKey: keyPair?.publicKey });
const { decrypt } = useDecrypt({ privateKey: keyPair?.privateKey });

const [ciphertext, setCiphertext] = useState('');
const [plaintext, setPlaintext] = useState('');

// On mount: restore from storage or generate a new key pair
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
importKeyPair(JSON.parse(saved));
} else {
generate();
}
}, []);

// Whenever the serialized key pair changes, persist it
useEffect(() => {
if (serialized) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(serialized));
}
}, [serialized]);

if (generating) return <p>Setting up keys…</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
<button
onClick={async () => setCiphertext(await encrypt('Hello from RSA!'))}
disabled={!keyPair}
>
Encrypt
</button>

<button
onClick={async () => setPlaintext(await decrypt(ciphertext))}
disabled={!ciphertext}
>
Decrypt
</button>

{ciphertext && <p><strong>Ciphertext:</strong> {ciphertext.slice(0, 48)}</p>}
{plaintext && <p><strong>Plaintext:</strong> {plaintext}</p>}
</div>
);
}
info

serialized.publicKey (Base64 SPKI) and serialized.privateKey (Base64 PKCS8) are plain strings — store them anywhere: localStorage, sessionStorage, a database, or IndexedDB.


AES-GCM: generate, persist, and re-import a symmetric key

import { useSymmetricKey } from 'react-e2ee';
import { useEffect, useState } from 'react';

const STORAGE_KEY = 'myapp:aes-key';

export function PersistentAesDemo() {
const { key, exportedKey, generating, generate, importKey, encrypt, decrypt, error } =
useSymmetricKey();

const [ciphertext, setCiphertext] = useState('');
const [plaintext, setPlaintext] = useState('');

// On mount: restore or generate
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
importKey(saved);
} else {
generate();
}
}, []);

// Persist whenever the exported key changes
useEffect(() => {
if (exportedKey) {
localStorage.setItem(STORAGE_KEY, exportedKey);
}
}, [exportedKey]);

if (generating) return <p>Setting up key…</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
<button
onClick={async () => setCiphertext(await encrypt('AES-GCM payload'))}
disabled={!key}
>
Encrypt
</button>

<button
onClick={async () => setPlaintext(await decrypt(ciphertext))}
disabled={!ciphertext}
>
Decrypt
</button>

{plaintext && <p><strong>Plaintext:</strong> {plaintext}</p>}
</div>
);
}

Hybrid encryption: RSA + AES

Use RSA to protect the AES key, and AES to encrypt the actual data. This is the standard pattern for E2E systems that need to encrypt arbitrarily large payloads — RSA alone is limited to a small amount of data per call (a few hundred bytes with a 2048-bit key).

import {
useKeyPair,
aesEncrypt, aesDecrypt,
generateAesKey, exportAesKey, importAesKey,
} from 'react-e2ee';
import { useEncrypt, useDecrypt } from 'react-e2ee';

export function HybridDemo() {
const { keyPair, generate, generating } = useKeyPair();
const { encrypt: rsaEncrypt } = useEncrypt({ publicKey: keyPair?.publicKey });
const { decrypt: rsaDecrypt } = useDecrypt({ privateKey: keyPair?.privateKey });

const [output, setOutput] = useState<{ wrappedKey: string; body: string } | null>(null);
const [plaintext, setPlaintext] = useState('');

const handleEncrypt = async () => {
// 1. Generate a one-time AES key for this message
const aesKey = await generateAesKey();
const rawAesKey = await exportAesKey(aesKey);

// 2. Encrypt the payload with AES
const body = await aesEncrypt(aesKey, 'Very large payload — encrypt with AES!');

// 3. Wrap the AES key with RSA so only the private-key holder can read it
const wrappedKey = await rsaEncrypt(rawAesKey);

setOutput({ wrappedKey, body });
};

const handleDecrypt = async () => {
if (!output) return;

// 1. Unwrap the AES key with RSA private key
const rawAesKey = await rsaDecrypt(output.wrappedKey);

// 2. Import the AES key and decrypt the body
const aesKey = await importAesKey(rawAesKey);
const pt = await aesDecrypt(aesKey, output.body);

setPlaintext(pt);
};

return (
<div>
<button onClick={generate} disabled={generating}>Generate RSA key pair</button>
<button onClick={handleEncrypt} disabled={!keyPair}>Hybrid Encrypt</button>
<button onClick={handleDecrypt} disabled={!output}>Hybrid Decrypt</button>
{plaintext && <p><strong>Plaintext:</strong> {plaintext}</p>}
</div>
);
}

Cross-device encryption with password derivation

Because deriveKeyFromSeed is deterministic, the same password and identifier always produce the same AES key — on any device, with no data transfer.

Device A — first-time setup

import { useSeedKey } from 'react-e2ee';

export function SetupEncryption({ userId }: { userId: string }) {
const { hasKey, loading, deriveAndSave } = useSeedKey(userId);
const [password, setPassword] = useState('');
const [ready, setReady] = useState(false);

if (loading) return <p>Checking device for saved key…</p>;

if (hasKey) return <p>Encryption ready — key loaded from this device.</p>;

return (
<div>
<input
type="password"
placeholder="Choose an encryption password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button
onClick={async () => {
await deriveAndSave(password);
setReady(true);
}}
>
Set up encryption
</button>
{ready && <p>Key saved. You can now encrypt data.</p>}
</div>
);
}

Device B — same password, same key

import { useSeedKey, aesDecrypt } from 'react-e2ee';

export function DecryptOnNewDevice({
userId,
ciphertext,
}: {
userId: string;
ciphertext: string;
}) {
const { hasKey, key, loading, deriveAndSave } = useSeedKey(userId);
const [password, setPassword] = useState('');
const [plaintext, setPlaintext] = useState('');

if (loading) return <p>Checking device for saved key…</p>;

if (!hasKey) {
return (
<div>
<p>No key on this device. Enter your password to restore access.</p>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button onClick={() => deriveAndSave(password)}>Restore key</button>
</div>
);
}

const handleDecrypt = async () => {
if (!key) return;
const pt = await aesDecrypt(key, ciphertext);
setPlaintext(pt);
};

return (
<div>
<button onClick={handleDecrypt}>Decrypt</button>
{plaintext && <p><strong>Plaintext:</strong> {plaintext}</p>}
</div>
);
}
How same-key derivation works

deriveKeyFromSeed(password, identifier) runs PBKDF2 with:

  • Password: what the user entered
  • Salt: SHA-256 hash of the identifier (deterministic — no stored salt needed)
  • Iterations: 600 000 (OWASP 2024 recommendation)
  • Output: 256-bit AES-GCM key

The SHA-256 salt ensures the same password used with different identifiers produces different keys, while keeping derivation identical across devices.


Encrypting non-string data

All encrypt functions accept both strings and ArrayBuffer. Use encodeText and decodeText for explicit control, or pass binary data directly.

import { generateAesKey, aesEncrypt, aesDecrypt, encodeText, decodeText } from 'react-e2ee';

const key = await generateAesKey();

// String shorthand
const ct1 = await aesEncrypt(key, 'hello');

// ArrayBuffer directly (e.g. from a File or fetch response)
const buffer: ArrayBuffer = await fetch('/api/blob').then(r => r.arrayBuffer());
const ct2 = await aesEncrypt(key, buffer);

// Decrypt always returns a string via UTF-8 decode
const plaintext = await aesDecrypt(key, ct1);

Secret exchange between users

Use useSecretExchange when one user wants to share an encrypted secret with another user. The target user's RSA public key is the only thing needed on the sender's side — the target's private key never leaves their device.

Single message hand-off (encryptFor)

Ideal for sharing one specific ciphertext with another user.

import { useSecretExchange, useKeyPair, useDecrypt } from 'react-e2ee';

// ─── Sender (Alice) ───────────────────────────────────────────────────────────
export function AliceSender({ aliceCiphertext, bobPublicKey }) {
// Loads Alice's AES key from IndexedDB by her identifier
const { encryptFor, loading, error } = useSecretExchange('user-alice');
const [envelope, setEnvelope] = useState('');

if (loading) return <p>Loading Alice's key…</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<button
onClick={async () => {
// Decrypt with Alice's AES key → re-encrypt for Bob with his RSA public key
const env = await encryptFor(aliceCiphertext, bobPublicKey);
setEnvelope(env);
// Send `env` to Bob via your API
await fetch('/api/messages', { method: 'POST', body: JSON.stringify({ envelope: env }) });
}}
>
Share with Bob
</button>
);
}

// ─── Recipient (Bob) ──────────────────────────────────────────────────────────
export function BobReceiver({ envelope }) {
const { keyPair, generate, generating } = useKeyPair();
const { decrypt } = useDecrypt({ privateKey: keyPair?.privateKey });
const [plaintext, setPlaintext] = useState('');

return (
<div>
{!keyPair && (
<button onClick={generate} disabled={generating}>
Generate Bob's key pair
</button>
)}
<button
onClick={async () => setPlaintext(await decrypt(envelope))}
disabled={!keyPair || !envelope}
>
Decrypt
</button>
{plaintext && <p><strong>Message:</strong> {plaintext}</p>}
</div>
);
}

Full access grant (wrapKeyFor)

Ideal for granting another user ongoing read access to all messages encrypted with the sender's AES key.

import {
useSecretExchange,
useDecrypt,
importAesKey,
aesDecrypt,
} from 'react-e2ee';

// ─── Alice grants Bob access to her AES key ───────────────────────────────────
export function AliceGrantAccess({ bobPublicKey }) {
const { wrapKeyFor, loading } = useSecretExchange('user-alice');

return (
<button
onClick={async () => {
// RSA-wrap Alice's AES key bytes for Bob
const wrappedKey = await wrapKeyFor(bobPublicKey);
// Send wrappedKey + any ciphertexts to Bob via your API
await fetch('/api/grant', {
method: 'POST',
body: JSON.stringify({ wrappedKey }),
});
}}
disabled={loading}
>
Grant Bob access
</button>
);
}

// ─── Bob unwraps the AES key and decrypts Alice's messages ───────────────────
export function BobDecryptAliceMessages({ wrappedKey, aliceCiphertexts }) {
const { decrypt: rsaDecrypt } = useDecrypt({ privateKey: bobKeyPair.privateKey });
const [messages, setMessages] = useState([]);

const handleDecrypt = async () => {
// 1. Unwrap Alice's AES key with Bob's RSA private key
const rawAesKey = await rsaDecrypt(wrappedKey);

// 2. Import the raw AES key
const aesKey = await importAesKey(rawAesKey);

// 3. Decrypt all of Alice's messages
const decrypted = await Promise.all(
aliceCiphertexts.map(ct => aesDecrypt(aesKey, ct))
);
setMessages(decrypted);
};

return (
<div>
<button onClick={handleDecrypt}>Decrypt all of Alice's messages</button>
{messages.map((msg, i) => <p key={i}>{msg}</p>)}
</div>
);
}
How it works

encryptFor performs a local decrypt → re-encrypt cycle:

  1. AES-decrypt the ciphertext with the sender's key (IndexedDB lookup by identifier)
  2. RSA-OAEP encrypt the plaintext with the target's public key

wrapKeyFor skips the decrypt step and RSA-wraps the raw AES key bytes directly — more efficient when sharing many messages at once.


Platform emergency access

Use usePlatformAccess to let your platform decrypt any message in an emergency — without affecting normal E2E confidentiality. The platform publishes its RSA public key in your app config; the private key never leaves the platform's secure infrastructure.

Encrypt with platform access (one-call pattern)

import {
usePlatformAccess,
useEncrypt,
} from 'react-e2ee';
import { useState } from 'react';

// Embed the platform's RSA public key (Base64 SPKI) in your app config.
// The matching private key lives only in your secure platform infrastructure.
const PLATFORM_PUBLIC_KEY = import.meta.env.VITE_PLATFORM_PUBLIC_KEY;

interface MessageRecord {
ciphertext: string; // AES-GCM encrypted payload
platformEnvelope: string; // AES key RSA-wrapped for the platform
recipientEnvelope: string; // AES key RSA-wrapped for the recipient
}

export function SendWithPlatformAccess({ recipientPublicKey }: { recipientPublicKey: CryptoKey }) {
const { encryptWithAccess, ready, loading, error } = usePlatformAccess(PLATFORM_PUBLIC_KEY);
const { encrypt: encryptForRecipient } = useEncrypt({ publicKey: recipientPublicKey });
const [record, setRecord] = useState<MessageRecord | null>(null);

const handleSend = async () => {
// 1. Encrypt data + wrap AES key for the platform in one call
const { ciphertext, platformEnvelope, exportedKey } =
await encryptWithAccess('Confidential message');

// 2. Also wrap the same AES key for the intended recipient
const recipientEnvelope = await encryptForRecipient(exportedKey);

// 3. Persist all three together (e.g. send to your API)
const msg: MessageRecord = { ciphertext, platformEnvelope, recipientEnvelope };
setRecord(msg);
await fetch('/api/messages', { method: 'POST', body: JSON.stringify(msg) });
};

if (loading) return <p>Loading platform key…</p>;
if (error) return <p>Platform key error: {error.message}</p>;

return (
<div>
<button onClick={handleSend} disabled={!ready}>
Send (with platform access)
</button>
{record && <p>Message stored. Platform envelope: {record.platformEnvelope.slice(0, 32)}</p>}
</div>
);
}

Wrap an existing AES key for the platform

If you already have an AES key from useSymmetricKey or useKeyStorage, use wrapKey to produce the envelope separately:

import { usePlatformAccess, useSymmetricKey } from 'react-e2ee';
import { useState } from 'react';

export function WrapExistingKey() {
const PLATFORM_PUBLIC_KEY = import.meta.env.VITE_PLATFORM_PUBLIC_KEY;
const { wrapKey, ready } = usePlatformAccess(PLATFORM_PUBLIC_KEY);
const { key, generate, encrypt } = useSymmetricKey();
const [envelope, setEnvelope] = useState('');

const handleEncryptAndWrap = async () => {
if (!key) return;
const ciphertext = await encrypt('payload');
const platformEnvelope = await wrapKey(key);
setEnvelope(platformEnvelope);
// Store { ciphertext, platformEnvelope } together
};

return (
<div>
<button onClick={generate}>Generate AES key</button>
<button onClick={handleEncryptAndWrap} disabled={!ready || !key}>
Encrypt + wrap for platform
</button>
{envelope && <p>Platform envelope ready ({envelope.length} chars)</p>}
</div>
);
}
Emergency access flow (platform side)

When the platform needs emergency access, it:

  1. Decrypts platformEnvelope with its RSA private key using rsaDecrypt
  2. Imports the recovered Base64 AES key via importAesKey
  3. Decrypts the ciphertext with aesDecrypt

The platform's private key must never be exposed to the browser. Keep it in secure server infrastructure (HSM, KMS, or a secrets manager).


Error handling

Every hook exposes an error state and async methods reject on failure. Handle both for a robust UI:

const { decrypt, decrypting, error } = useDecrypt({ privateKey: keyPair?.privateKey });

const handleDecrypt = async () => {
try {
const plaintext = await decrypt(ciphertext);
setPlaintext(plaintext);
} catch (err) {
// err is always an Error instance
console.error('Decryption failed:', err);
}
};

// Render the hook-level error in the UI
{error && <p className="error">Error: {error.message}</p>}

HMAC message authentication

Use useHmac to generate an HMAC key, sign data, and verify signatures. HMAC proves data was created by someone who holds the same secret key — it detects tampering.

import { useState } from 'react';
import { useHmac } from 'react-e2ee';

export function HmacDemo() {
const { generate, sign, verify, exportedKey, key, error } = useHmac();
const [message, setMessage] = useState('Authenticate this message');
const [signature, setSignature] = useState('');
const [verified, setVerified] = useState<boolean | null>(null);

return (
<div>
<button onClick={() => generate()}>Generate HMAC Key</button>

<input value={message} onChange={e => setMessage(e.target.value)} />

<button
onClick={async () => {
const sig = await sign(message);
setSignature(sig);
setVerified(null);
}}
disabled={!key}
>
Sign
</button>

<button
onClick={async () => {
const ok = await verify(signature, message);
setVerified(ok);
}}
disabled={!signature}
>
Verify
</button>

{signature && <p><strong>Signature:</strong> {signature.slice(0, 40)}</p>}
{verified !== null && (
<p>{verified ? '✅ Valid' : '❌ Invalid — data was tampered with!'}</p>
)}
</div>
);
}

ECDSA digital signatures

useSign provides ECDSA key pair management with sign and verify. Use it to authenticate the sender of a message alongside encryption.

import { useState } from 'react';
import { useSign, useKeyPair, useEncrypt, useDecrypt } from 'react-e2ee';

export function SignedEncryptionDemo() {
const signing = useSign();
const { keyPair, generate: generateRsa } = useKeyPair();
const { encrypt } = useEncrypt({ publicKey: keyPair?.publicKey });
const { decrypt } = useDecrypt({ privateKey: keyPair?.privateKey });
const [output, setOutput] = useState<{ ciphertext: string; signature: string } | null>(null);
const [verified, setVerified] = useState<boolean | null>(null);

const handleSignAndEncrypt = async () => {
const message = 'Authenticated and encrypted message';
const ciphertext = await encrypt(message);
const signature = await signing.sign(ciphertext); // sign the ciphertext
setOutput({ ciphertext, signature });
setVerified(null);
};

const handleVerifyAndDecrypt = async () => {
if (!output) return;
// Verify the signature first
const ok = await signing.verify(output.signature, output.ciphertext);
setVerified(ok);
if (ok) {
const plaintext = await decrypt(output.ciphertext);
console.log('Decrypted:', plaintext);
}
};

return (
<div>
<button onClick={() => { generateRsa(); signing.generate(); }}>
Generate Keys (RSA + ECDSA)
</button>
<button onClick={handleSignAndEncrypt} disabled={!keyPair || !signing.keyPair}>
Sign & Encrypt
</button>
<button onClick={handleVerifyAndDecrypt} disabled={!output}>
Verify & Decrypt
</button>
{verified !== null && (
<p>{verified ? '✅ Signature valid — sender authenticated' : '❌ Signature invalid!'}</p>
)}
</div>
);
}

Key fingerprinting for out-of-band verification

useFingerprint computes a human-readable fingerprint of any key, perfect for comparing via phone call, QR code, or in-person.

import { useFingerprint, useKeyPair } from 'react-e2ee';
import { useState } from 'react';

export function FingerprintDemo() {
const { keyPair, generate } = useKeyPair();
const { fingerprint } = useFingerprint();
const [fp, setFp] = useState('');

return (
<div>
<button onClick={generate}>Generate Key Pair</button>
<button
onClick={async () => {
if (keyPair) setFp(await fingerprint(keyPair.publicKey));
}}
disabled={!keyPair}
>
Compute Fingerprint
</button>
{fp && (
<p style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>
{fp}
</p>
)}
</div>
);
}
Out-of-band verification

Have both parties compute their public key fingerprints and compare them through a trusted channel (phone, video call, or in person). If they match, you can be confident no MITM substitution has occurred.