TypeScript Platform Guide
This guide covers the installation, setup, and usage of MetaMUI Crypto Primitives in TypeScript/JavaScript projects.
Installation
Build from source:
cd metamui-crypto-typescript
npm install
npm run build
To use the WASM-based Falcon module specifically:
cd metamui-crypto-typescript/packages/falcon
npm install
npm run build
Running Tests
cd metamui-crypto-typescript/packages/falcon
npx vitest run
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,
},
}),
],
});
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
}
}
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;
}
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>
);
}
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' });
}
};
}
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);
});
});
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'); }