import * as React from "react"; import * as Comlink from "comlink"; type CryptoContextType = { decrypt: ( secret: string, input: Uint8Array, ) => Promise>; decryptText: (secret: string, ciphertext: string) => Promise; encrypt: ( secret: string, input: Uint8Array, ) => Promise>; encryptText: (secret: string, plaintext: string) => Promise; getSharedSecret: ( ownPrivateKey: string, ownPublicKey: string, otherPublicKey: string, ) => Promise; }; type ApiRef = { encrypt: ( secret: string, input: Uint8Array, ) => Promise>; decrypt: ( secret: string, input: Uint8Array, ) => Promise>; decryptText: (secret: string, ciphertext: string) => Promise; encryptText: (secret: string, plaintext: string) => Promise; getSharedSecret: ( ownPrivateKey: string, ownPublicKey: string, otherPublicKey: string, ) => Promise; }; export function bytesToBase64(bytes: Uint8Array): string { let binary = ""; for (const b of bytes) binary += String.fromCharCode(b); return btoa(binary); } export function base64ToBytes(base64: string): Uint8Array { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } export const context = React.createContext( undefined, ); /** * Provides cryptographic actions backed by a worker without coupling to UI state. * @param props Component props with children. * @returns Crypto context provider JSX. */ export default function Provider(props: { children: React.ReactNode }) { const apiRef = React.useRef(null); const value = React.useMemo( () => ({ encrypt: async (secret, plaintext) => { const api = apiRef.current; if (!api) throw new Error("API not initialized"); return await api.encrypt(secret, plaintext); }, decrypt: async (secret, ciphertext) => { const api = apiRef.current; if (!api) throw new Error("API not initialized"); return await api.decrypt(secret, ciphertext); }, encryptText: async (secret, plaintext) => { const api = apiRef.current; if (!api) throw new Error("API not initialized"); return await api.encryptText(secret, plaintext); }, decryptText: async (secret, ciphertext) => { const api = apiRef.current; if (!api) throw new Error("API not initialized"); return await api.decryptText(secret, ciphertext); }, getSharedSecret: async (ownPrivateKey, ownPublicKey, otherPublicKey) => { const api = apiRef.current; if (!api) throw new Error("API not initialized"); return await api.getSharedSecret( ownPrivateKey, ownPublicKey, otherPublicKey, ); }, }), [], ); React.useEffect(() => { const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", }); apiRef.current = Comlink.wrap(worker); return () => { apiRef.current = null; worker.terminate(); }; }, []); return {props.children}; } /** * Creates crypto action functions that safely delegate to the worker API. * @param getApiRef Function that returns the worker API reference. * @returns Typed crypto action functions. */ export function createCryptoActions( getApiRef: () => ApiRef | null, ): CryptoContextType { /** * Encrypts bytes by delegating to the crypto worker API. * @param secret Hex-encoded shared secret. * @param input Plaintext bytes to encrypt. * @returns Ciphertext bytes. */ const encrypt = async ( secret: string, input: Uint8Array, ): Promise> => { const api = getApiRef(); if (!api) throw new Error("API not initialized"); return await api.encrypt(secret, input); }; /** * Decrypts bytes by delegating to the crypto worker API. * @param secret Hex-encoded shared secret. * @param input Ciphertext bytes to decrypt. * @returns Plaintext bytes. */ const decrypt = async ( secret: string, input: Uint8Array, ): Promise> => { const api = getApiRef(); if (!api) throw new Error("API not initialized"); return await api.decrypt(secret, input); }; /** * Encrypts plaintext text by delegating to the crypto worker API. * @param secret Hex-encoded shared secret. * @param plaintext Plaintext to encrypt. * @returns Base64 ciphertext. */ const encryptText = async ( secret: string, plaintext: string, ): Promise => { const api = getApiRef(); if (!api) throw new Error("API not initialized"); return await api.encryptText(secret, plaintext); }; /** * Decrypts base64 ciphertext text by delegating to the crypto worker API. * @param secret Hex-encoded shared secret. * @param ciphertext Base64 ciphertext to decrypt. * @returns Decrypted plaintext. */ const decryptText = async ( secret: string, ciphertext: string, ): Promise => { const api = getApiRef(); if (!api) throw new Error("API not initialized"); return await api.decryptText(secret, ciphertext); }; /** * Derives a shared secret from local and peer key material via the worker API. * @param ownPrivateKey Local private key. * @param ownPublicKey Local public key. * @param otherPublicKey Peer public key. * @returns Hex-encoded shared secret. */ const getSharedSecret = async ( ownPrivateKey: string, ownPublicKey: string, otherPublicKey: string, ): Promise => { const api = getApiRef(); if (!api) throw new Error("API not initialized"); return await api.getSharedSecret( ownPrivateKey, ownPublicKey, otherPublicKey, ); }; return { encrypt, decrypt, encryptText, decryptText, getSharedSecret }; } /** * Returns the crypto actions from the nearest provider. * Throws when used outside of the crypto provider tree. */ export function useCrypto(): CryptoContextType { const ctx = React.useContext(context); if (!ctx) { throw new Error("useCrypto must be used within a CryptoProvider"); } return ctx; }