129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import * as React from "react";
|
|
import * as Comlink from "comlink";
|
|
|
|
type CryptoContextType = {
|
|
decrypt: (secret: string, ciphertext: string) => Promise<string>;
|
|
encrypt: (secret: string, plaintext: string) => Promise<string>;
|
|
getSharedSecret: (
|
|
ownPrivateKey: string,
|
|
ownPublicKey: string,
|
|
otherPublicKey: string,
|
|
) => Promise<string>;
|
|
};
|
|
|
|
type ApiRef = {
|
|
encrypt: (secret: string, plaintext: string) => Promise<string>;
|
|
decrypt: (secret: string, ciphertext: string) => Promise<string>;
|
|
getSharedSecret: (
|
|
ownPrivateKey: string,
|
|
ownPublicKey: string,
|
|
otherPublicKey: string,
|
|
) => Promise<string>;
|
|
};
|
|
|
|
export const context = React.createContext<CryptoContextType | undefined>(
|
|
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<ApiRef | null>(null);
|
|
|
|
const value = React.useMemo(
|
|
() => createCryptoActions(() => apiRef.current),
|
|
[],
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
|
|
type: "module",
|
|
});
|
|
|
|
apiRef.current = Comlink.wrap<ApiRef>(worker);
|
|
|
|
return () => {
|
|
apiRef.current = null;
|
|
worker.terminate();
|
|
};
|
|
}, []);
|
|
|
|
return <context.Provider value={value}>{props.children}</context.Provider>;
|
|
}
|
|
|
|
/**
|
|
* Creates crypto action functions that safely delegate to the worker API.
|
|
* @param getApiRef Function that returns worker API reference when initialized.
|
|
* @returns Typed crypto action functions.
|
|
*/
|
|
export function createCryptoActions(
|
|
getApiRef: () => ApiRef | null,
|
|
): CryptoContextType {
|
|
/**
|
|
* Encrypts plaintext by delegating to the crypto worker API.
|
|
* @param secret Hex-encoded shared secret.
|
|
* @param plaintext Plaintext to encrypt.
|
|
* @returns Encrypted ciphertext.
|
|
*/
|
|
const encrypt = async (
|
|
secret: string,
|
|
plaintext: string,
|
|
): Promise<string> => {
|
|
const apiRef = getApiRef();
|
|
if (!apiRef) throw new Error("API not initialized");
|
|
return await apiRef.encrypt(secret, plaintext);
|
|
};
|
|
|
|
/**
|
|
* Decrypts ciphertext by delegating to the crypto worker API.
|
|
* @param secret Hex-encoded shared secret.
|
|
* @param ciphertext Ciphertext to decrypt.
|
|
* @returns Decrypted plaintext.
|
|
*/
|
|
const decrypt = async (
|
|
secret: string,
|
|
ciphertext: string,
|
|
): Promise<string> => {
|
|
const apiRef = getApiRef();
|
|
if (!apiRef) throw new Error("API not initialized");
|
|
return await apiRef.decrypt(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<string> => {
|
|
const apiRef = getApiRef();
|
|
if (!apiRef) throw new Error("API not initialized");
|
|
return await apiRef.getSharedSecret(
|
|
ownPrivateKey,
|
|
ownPublicKey,
|
|
otherPublicKey,
|
|
);
|
|
};
|
|
|
|
return { encrypt, decrypt, 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;
|
|
}
|