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

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

  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