TypeScript Platform Guide
This guide covers the installation, setup, and usage of MetaMUI Crypto Primitives in TypeScript/JavaScript projects.
Installation
Install via npm or yarn:
# npm
npm install @metamui/crypto
# yarn
yarn add @metamui/crypto
# pnpm
pnpm add @metamui/crypto
Or install specific algorithms:
npm install @metamui/crypto-aes @metamui/crypto-chacha20 @metamui/crypto-ed25519
Quick Start
import { Ed25519, ChaCha20Poly1305 } from '@metamui/crypto';
async function main() {
// Generate Ed25519 keypair
const keypair = await Ed25519.generateKeypair();
// Sign a message
const message = new TextEncoder().encode('Hello, MetaMUI!');
const signature = await Ed25519.sign(message, keypair.privateKey);
// Verify signature
const isValid = await Ed25519.verify(signature, message, keypair.publicKey);
console.log('Signature valid:', isValid);
// Encrypt with ChaCha20-Poly1305
const key = await ChaCha20Poly1305.generateKey();
const cipher = new ChaCha20Poly1305(key);
const plaintext = new TextEncoder().encode('Secret message');
const nonce = ChaCha20Poly1305.generateNonce();
const { ciphertext, tag } = await cipher.encrypt(plaintext, nonce);
// Decrypt
const decrypted = await cipher.decrypt(ciphertext, tag, nonce);
console.log('Decrypted:', new TextDecoder().decode(decrypted));
}
main().catch(console.error);
Browser Support
Using with Webpack
// webpack.config.js
module.exports = {
resolve: {
fallback: {
"crypto": require.resolve("crypto-browserify"),
"stream": require.resolve("stream-browserify"),
"buffer": require.resolve("buffer/")
}
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
})
]
};
Using with Vite
// vite.config.js
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
export default defineConfig({
plugins: [
nodePolyfills({
globals: {
Buffer: true,
global: true,
process: true,
},
}),
],
});
Direct Browser Usage
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/@metamui/crypto/dist/browser.min.js"></script>
</head>
<body>
<script>
async function demo() {
const { Ed25519 } = MetaMUICrypto;
const keypair = await Ed25519.generateKeypair();
console.log('Public key:', keypair.publicKey);
}
demo();
</script>
</body>
</html>
TypeScript Configuration
Recommended tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["node", "@metamui/crypto"]
}
}
Common Patterns
Error Handling
import { Ed25519, CryptoError } from '@metamui/crypto';
async function signSafely(
message: Uint8Array,
privateKey: Uint8Array
): Promise<Uint8Array | null> {
try {
return await Ed25519.sign(message, privateKey);
} catch (error) {
if (error instanceof CryptoError) {
console.error('Crypto error:', error.code, error.message);
switch (error.code) {
case 'INVALID_KEY_LENGTH':
console.error('Key must be 32 bytes');
break;
case 'INVALID_SIGNATURE':
console.error('Signature verification failed');
break;
default:
console.error('Unknown crypto error');
}
}
return null;
}
}
Working with Buffers
import { ChaCha20Poly1305, toBuffer, fromHex, toHex } from '@metamui/crypto';
// Convert between formats
const hexKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const key = fromHex(hexKey);
// From string
const message = toBuffer('Hello, World!', 'utf8');
// From base64
const encoded = 'SGVsbG8sIFdvcmxkIQ==';
const decoded = toBuffer(encoded, 'base64');
// To hex
const cipher = new ChaCha20Poly1305(key);
const nonce = ChaCha20Poly1305.generateNonce();
const { ciphertext, tag } = await cipher.encrypt(message, nonce);
console.log('Ciphertext:', toHex(ciphertext));
console.log('Tag:', toHex(tag));
Async/Await vs Callbacks
import { Argon2 } from '@metamui/crypto';
// Modern async/await
async function deriveKeyAsync(password: string): Promise<Uint8Array> {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await Argon2.hash(password, salt, {
timeCost: 3,
memoryCost: 65536,
parallelism: 4,
hashLength: 32
});
return key;
}
// Legacy callback style (if needed)
function deriveKeyCallback(
password: string,
callback: (err: Error | null, key?: Uint8Array) => void
): void {
deriveKeyAsync(password)
.then(key => callback(null, key))
.catch(err => callback(err));
}
// Promise style
function deriveKeyPromise(password: string): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
deriveKeyAsync(password)
.then(resolve)
.catch(reject);
});
}
React Integration
Custom Hooks
import { useState, useEffect } from 'react';
import { Ed25519, Keypair } from '@metamui/crypto';
export function useKeypair() {
const [keypair, setKeypair] = useState<Keypair | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
Ed25519.generateKeypair()
.then(setKeypair)
.catch(setError)
.finally(() => setLoading(false));
}, []);
const regenerate = async () => {
setLoading(true);
try {
const newKeypair = await Ed25519.generateKeypair();
setKeypair(newKeypair);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
return { keypair, loading, error, regenerate };
}
// Usage
function KeyDisplay() {
const { keypair, loading, error, regenerate } = useKeypair();
if (loading) return <div>Generating keypair...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!keypair) return null;
return (
<div>
<p>Public Key: {toHex(keypair.publicKey)}</p>
<button onClick={regenerate}>Generate New</button>
</div>
);
}
Encryption Component
import React, { useState } from 'react';
import { ChaCha20Poly1305, fromHex, toHex } from '@metamui/crypto';
interface EncryptionResult {
ciphertext: string;
tag: string;
nonce: string;
}
export function EncryptionDemo() {
const [message, setMessage] = useState('');
const [key, setKey] = useState('');
const [result, setResult] = useState<EncryptionResult | null>(null);
const handleEncrypt = async () => {
try {
const keyBytes = fromHex(key);
const cipher = new ChaCha20Poly1305(keyBytes);
const plaintext = new TextEncoder().encode(message);
const nonce = ChaCha20Poly1305.generateNonce();
const { ciphertext, tag } = await cipher.encrypt(plaintext, nonce);
setResult({
ciphertext: toHex(ciphertext),
tag: toHex(tag),
nonce: toHex(nonce)
});
} catch (error) {
console.error('Encryption failed:', error);
}
};
return (
<div>
<input
type="text"
placeholder="Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<input
type="text"
placeholder="Key (64 hex chars)"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
<button onClick={handleEncrypt}>Encrypt</button>
{result && (
<div>
<p>Ciphertext: {result.ciphertext}</p>
<p>Tag: {result.tag}</p>
<p>Nonce: {result.nonce}</p>
</div>
)}
</div>
);
}
Node.js Integration
Express Middleware
import express from 'express';
import { Ed25519, ChaCha20Poly1305 } from '@metamui/crypto';
// Authentication middleware
export function authMiddleware(publicKey: Uint8Array) {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const signature = req.headers['x-signature'] as string;
const timestamp = req.headers['x-timestamp'] as string;
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing authentication headers' });
}
// Verify timestamp is recent (prevent replay attacks)
const now = Date.now();
const requestTime = parseInt(timestamp, 10);
if (Math.abs(now - requestTime) > 60000) { // 1 minute
return res.status(401).json({ error: 'Request expired' });
}
// Construct message to verify
const message = `${req.method}:${req.path}:${timestamp}:${JSON.stringify(req.body)}`;
const messageBytes = new TextEncoder().encode(message);
const signatureBytes = fromHex(signature);
try {
const isValid = await Ed25519.verify(signatureBytes, messageBytes, publicKey);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
} catch (error) {
return res.status(401).json({ error: 'Signature verification failed' });
}
};
}
// Encryption middleware
export function encryptionMiddleware(key: Uint8Array) {
const cipher = new ChaCha20Poly1305(key);
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
// Store original json method
const originalJson = res.json.bind(res);
// Override json method to encrypt response
res.json = async function(data: any) {
const plaintext = new TextEncoder().encode(JSON.stringify(data));
const nonce = ChaCha20Poly1305.generateNonce();
const { ciphertext, tag } = await cipher.encrypt(plaintext, nonce);
return originalJson({
encrypted: true,
nonce: toHex(nonce),
tag: toHex(tag),
data: toHex(ciphertext)
});
};
next();
};
}
Worker Threads
import { Worker } from 'worker_threads';
import { Argon2 } from '@metamui/crypto';
// worker.ts
import { parentPort } from 'worker_threads';
parentPort?.on('message', async ({ password, salt, options }) => {
try {
const key = await Argon2.hash(password, salt, options);
parentPort?.postMessage({ success: true, key });
} catch (error) {
parentPort?.postMessage({ success: false, error: error.message });
}
});
// main.ts
export function deriveKeyInWorker(
password: string,
salt: Uint8Array
): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.on('message', ({ success, key, error }) => {
worker.terminate();
if (success) {
resolve(key);
} else {
reject(new Error(error));
}
});
worker.postMessage({
password,
salt,
options: {
timeCost: 3,
memoryCost: 65536,
parallelism: 4,
hashLength: 32
}
});
});
}
Testing
Jest Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: {
target: 'ES2020',
},
},
},
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};
// test/setup.ts
import { webcrypto } from 'crypto';
// Polyfill for Node.js < 16
if (!globalThis.crypto) {
globalThis.crypto = webcrypto as any;
}
Unit Tests
import { Ed25519, ChaCha20Poly1305 } from '@metamui/crypto';
describe('Ed25519', () => {
it('should generate valid keypair', async () => {
const keypair = await Ed25519.generateKeypair();
expect(keypair.publicKey).toHaveLength(32);
expect(keypair.privateKey).toHaveLength(32);
});
it('should sign and verify', async () => {
const keypair = await Ed25519.generateKeypair();
const message = new TextEncoder().encode('test message');
const signature = await Ed25519.sign(message, keypair.privateKey);
expect(signature).toHaveLength(64);
const isValid = await Ed25519.verify(signature, message, keypair.publicKey);
expect(isValid).toBe(true);
});
it('should reject invalid signatures', async () => {
const keypair = await Ed25519.generateKeypair();
const message = new TextEncoder().encode('test message');
const signature = await Ed25519.sign(message, keypair.privateKey);
// Corrupt signature
signature[0] ^= 0xFF;
const isValid = await Ed25519.verify(signature, message, keypair.publicKey);
expect(isValid).toBe(false);
});
});
describe('ChaCha20Poly1305', () => {
it('should encrypt and decrypt', async () => {
const key = await ChaCha20Poly1305.generateKey();
const cipher = new ChaCha20Poly1305(key);
const plaintext = new TextEncoder().encode('secret message');
const nonce = ChaCha20Poly1305.generateNonce();
const aad = new TextEncoder().encode('additional data');
const { ciphertext, tag } = await cipher.encrypt(plaintext, nonce, aad);
const decrypted = await cipher.decrypt(ciphertext, tag, nonce, aad);
expect(decrypted).toEqual(plaintext);
});
});
Performance Optimization
Web Workers
// crypto-worker.ts
import { Ed25519, ChaCha20Poly1305 } from '@metamui/crypto';
self.addEventListener('message', async (event) => {
const { type, data } = event.data;
switch (type) {
case 'generateKeypair':
const keypair = await Ed25519.generateKeypair();
self.postMessage({ type: 'keypair', data: keypair });
break;
case 'encrypt':
const { plaintext, key, nonce } = data;
const cipher = new ChaCha20Poly1305(key);
const result = await cipher.encrypt(plaintext, nonce);
self.postMessage({ type: 'encrypted', data: result });
break;
}
});
// main.ts
export class CryptoWorkerPool {
private workers: Worker[] = [];
private taskQueue: Array<{
task: any;
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
constructor(size: number = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < size; i++) {
const worker = new Worker('./crypto-worker.js');
worker.addEventListener('message', this.handleMessage.bind(this));
this.workers.push(worker);
}
}
async generateKeypair(): Promise<any> {
return this.enqueue({ type: 'generateKeypair' });
}
async encrypt(plaintext: Uint8Array, key: Uint8Array, nonce: Uint8Array): Promise<any> {
return this.enqueue({
type: 'encrypt',
data: { plaintext, key, nonce }
});
}
private enqueue(task: any): Promise<any> {
return new Promise((resolve, reject) => {
this.taskQueue.push({ task, resolve, reject });
this.processQueue();
});
}
private processQueue(): void {
if (this.taskQueue.length === 0) return;
const worker = this.workers.find(w => !w.busy);
if (!worker) return;
const { task, resolve, reject } = this.taskQueue.shift()!;
worker.busy = true;
worker.currentResolve = resolve;
worker.currentReject = reject;
worker.postMessage(task);
}
private handleMessage(event: MessageEvent): void {
const worker = event.target as Worker;
worker.currentResolve(event.data.data);
worker.busy = false;
this.processQueue();
}
}
Streaming Large Files
import { ChaCha20Poly1305 } from '@metamui/crypto';
import { Transform } from 'stream';
export class EncryptionStream extends Transform {
private cipher: ChaCha20Poly1305;
private chunkIndex = 0;
constructor(key: Uint8Array, private baseNonce: Uint8Array) {
super();
this.cipher = new ChaCha20Poly1305(key);
}
async _transform(chunk: Buffer, encoding: string, callback: Function) {
try {
// Derive per-chunk nonce
const nonce = new Uint8Array(12);
nonce.set(this.baseNonce);
const indexBytes = new DataView(new ArrayBuffer(4));
indexBytes.setUint32(0, this.chunkIndex++, true);
nonce.set(new Uint8Array(indexBytes.buffer), 8);
// Encrypt chunk
const { ciphertext, tag } = await this.cipher.encrypt(chunk, nonce);
// Output: length + nonce + tag + ciphertext
const output = Buffer.allocUnsafe(4 + 12 + 16 + ciphertext.length);
output.writeUInt32LE(ciphertext.length, 0);
output.set(nonce, 4);
output.set(tag, 16);
output.set(ciphertext, 32);
callback(null, output);
} catch (error) {
callback(error);
}
}
}
// Usage
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
async function encryptFile(inputPath: string, outputPath: string, key: Uint8Array) {
const baseNonce = ChaCha20Poly1305.generateNonce();
await pipeline(
createReadStream(inputPath),
new EncryptionStream(key, baseNonce),
createWriteStream(outputPath)
);
}
Best Practices
- Always use TypeScript for better type safety
- Handle errors properly - crypto operations can fail
- Use secure random sources -
crypto.getRandomValues() - Clear sensitive data from memory when done
- Validate input lengths before crypto operations
- Use constant-time comparisons for sensitive data
- Enable strict mode in TypeScript configuration
Troubleshooting
Common Issues
- Buffer/Uint8Array confusion
// Convert Buffer to Uint8Array const uint8 = new Uint8Array(buffer); // Convert Uint8Array to Buffer const buffer = Buffer.from(uint8); - Async operation errors
// Always use try-catch with async crypto try { const result = await cryptoOperation(); } catch (error) { if (error.code === 'ERR_CRYPTO_INVALID_KEY') { // Handle specific error } } - Browser compatibility
// Check for Web Crypto API if (!globalThis.crypto?.subtle) { throw new Error('Web Crypto API not available'); }