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

{
    "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

  1. Always use TypeScript for better type safety
  2. Handle errors properly - crypto operations can fail
  3. Use secure random sources - crypto.getRandomValues()
  4. Clear sensitive data from memory when done
  5. Validate input lengths before crypto operations
  6. Use constant-time comparisons for sensitive data
  7. Enable strict mode in TypeScript configuration

Troubleshooting

Common Issues

  1. Buffer/Uint8Array confusion
    // Convert Buffer to Uint8Array
    const uint8 = new Uint8Array(buffer);
       
    // Convert Uint8Array to Buffer
    const buffer = Buffer.from(uint8);
    
  2. 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
        }
    }
    
  3. Browser compatibility
    // Check for Web Crypto API
    if (!globalThis.crypto?.subtle) {
        throw new Error('Web Crypto API not available');
    }
    

Resources