Files
Shade/demo.ts

156 lines
6.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* Shade E2EE Demo shows exactly how a frontend and backend use Shade.
*
* Run: bun demo.ts
*/
import { SubtleCryptoProvider, MemoryStorage } from './packages/shade-crypto-web/src/index.js';
import {
generateIdentityKeyPair,
generateSignedPreKey,
generateOneTimePreKeys,
createPreKeyBundle,
processPreKeyBundle,
processPreKeyMessage,
initSenderSession,
initReceiverSession,
ratchetEncrypt,
ratchetDecrypt,
} from './packages/shade-core/src/index.js';
const crypto = new SubtleCryptoProvider();
const enc = new TextEncoder();
const dec = new TextDecoder();
function log(who: string, msg: string) {
const color = who === 'FRONTEND' ? '\x1b[36m' : who === 'BACKEND' ? '\x1b[33m' : '\x1b[90m';
console.log(`${color}[${who}]\x1b[0m ${msg}`);
}
async function main() {
console.log('\n\x1b[1m=== SHADE E2EE DEMO ===\x1b[0m\n');
// ─── BACKEND: One-time setup (happens once on deploy) ──────
log('BACKEND', 'Generating identity keys...');
const backendStorage = new MemoryStorage();
const backendIdentity = await generateIdentityKeyPair(crypto);
await backendStorage.saveIdentityKeyPair(backendIdentity);
await backendStorage.saveLocalRegistrationId(1);
const signedPreKey = await generateSignedPreKey(crypto, backendIdentity, 1);
await backendStorage.saveSignedPreKey(signedPreKey);
const oneTimePreKeys = await generateOneTimePreKeys(crypto, 100, 10);
for (const otpk of oneTimePreKeys) await backendStorage.saveOneTimePreKey(otpk);
log('BACKEND', `Published prekey bundle (10 one-time prekeys ready)`);
// This bundle would be stored on the Shade Prekey Server
const bundle = createPreKeyBundle(1, backendIdentity, signedPreKey, oneTimePreKeys[0]);
// ─── FRONTEND: Connect to backend ─────────────────────────
console.log('\n\x1b[90m--- Frontend wants to talk to Backend ---\x1b[0m\n');
log('FRONTEND', 'Generating identity keys...');
const frontendStorage = new MemoryStorage();
const frontendIdentity = await generateIdentityKeyPair(crypto);
await frontendStorage.saveIdentityKeyPair(frontendIdentity);
log('FRONTEND', 'Fetching backend prekey bundle from Shade server...');
// In reality: const bundle = await fetch('https://shade-server/v1/keys/bundle/backend')
log('FRONTEND', 'Processing bundle (X3DH key agreement)...');
const x3dhResult = await processPreKeyBundle(crypto, frontendStorage, bundle);
log('FRONTEND', `Shared secret derived (${x3dhResult.rootKey.length * 8}-bit key)`);
// Frontend creates its ratchet session
const frontendSession = await initSenderSession(
crypto,
x3dhResult.rootKey,
x3dhResult.remoteIdentityKey,
x3dhResult.remoteSignedPreKey,
);
// ─── FRONTEND: Send first encrypted message ───────────────
console.log('\n\x1b[90m--- Encrypted conversation begins ---\x1b[0m\n');
const firstPlaintext = 'Hello Backend! This message is end-to-end encrypted.';
log('FRONTEND', `Encrypting: "${firstPlaintext}"`);
const firstEncrypted = await ratchetEncrypt(crypto, frontendSession, enc.encode(firstPlaintext));
// Bundle it as a PreKeyMessage (first message includes X3DH info)
const preKeyMessage = {
registrationId: 1,
preKeyId: x3dhResult.preKeyId,
signedPreKeyId: x3dhResult.signedPreKeyId,
ephemeralKey: x3dhResult.ephemeralPublicKey,
identityDHKey: frontendIdentity.dhPublicKey,
message: firstEncrypted,
};
log('FRONTEND', `Sending PreKeyMessage (${firstEncrypted.ciphertext.length} bytes ciphertext)`);
// In reality: await fetch('https://backend/api/messages', { body: preKeyMessage })
// ─── BACKEND: Receive and decrypt ─────────────────────────
log('BACKEND', 'Received PreKeyMessage, processing X3DH...');
const backendX3dh = await processPreKeyMessage(crypto, backendStorage, preKeyMessage);
log('BACKEND', `Shared secret derived (matches frontend: ${arrEq(backendX3dh.rootKey, x3dhResult.rootKey)})`);
const backendSession = initReceiverSession(
backendX3dh.rootKey,
backendX3dh.remoteIdentityKey,
signedPreKey.keyPair,
);
const decrypted1 = await ratchetDecrypt(crypto, backendSession, backendX3dh.initialMessage);
log('BACKEND', `Decrypted: "${dec.decode(decrypted1)}"`);
// ─── BACKEND: Reply (triggers DH ratchet) ──────────────────
const replyText = 'Got it! Every message from now on uses a unique key.';
log('BACKEND', `Encrypting reply: "${replyText}"`);
const replyEncrypted = await ratchetEncrypt(crypto, backendSession, enc.encode(replyText));
log('BACKEND', `Sending RatchetMessage (new DH key generated)`);
// ─── FRONTEND: Receive reply ──────────────────────────────
const decrypted2 = await ratchetDecrypt(crypto, frontendSession, replyEncrypted);
log('FRONTEND', `Decrypted: "${dec.decode(decrypted2)}"`);
// ─── A few more messages ──────────────────────────────────
console.log('\n\x1b[90m--- Continued conversation (each message = new key) ---\x1b[0m\n');
const conversation = [
{ from: 'FRONTEND', text: 'If someone steals this key, they cannot read previous messages.' },
{ from: 'BACKEND', text: 'Correct — that is forward secrecy via the Double Ratchet.' },
{ from: 'FRONTEND', text: 'And if they steal my device, future messages re-secure automatically?' },
{ from: 'BACKEND', text: 'Yes — the next DH ratchet step generates fresh keys. Post-compromise recovery.' },
];
for (const { from, text } of conversation) {
const sender = from === 'FRONTEND' ? frontendSession : backendSession;
const receiver = from === 'FRONTEND' ? backendSession : frontendSession;
const receiverName = from === 'FRONTEND' ? 'BACKEND' : 'FRONTEND';
const encrypted = await ratchetEncrypt(crypto, sender, enc.encode(text));
const plain = dec.decode(await ratchetDecrypt(crypto, receiver, encrypted));
log(from, `>> "${text}"`);
log(receiverName, `<< decrypted OK`);
}
// ─── Summary ──────────────────────────────────────────────
console.log('\n\x1b[1m=== SUMMARY ===\x1b[0m');
console.log(` Messages exchanged: 6`);
console.log(` DH ratchet steps: ${4} (new keys generated)`);
console.log(` One-time prekeys remaining: ${await backendStorage.getOneTimePreKeyCount()}`);
console.log(` Forward secrecy: YES`);
console.log(` Post-compromise: YES`);
console.log(` Plaintext leaked: ZERO\n`);
}
function arrEq(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
main().catch(console.error);