const REPLAY_DOMAIN = "nekonest-cloud/device-registration-replay/v1"; export const DEVICE_REGISTRATION_REPLAY_TTL_MS = 10 * 60_000; function bytesToBase64(bytes: Uint8Array): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary); } function base64ToBytes(value: string): Uint8Array { const binary = atob(value); return Uint8Array.from(binary, (character) => character.charCodeAt(0)); } async function registrationReplayKey(retryKey: string): Promise { const material = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(`${REPLAY_DOMAIN}\0${retryKey}`), ); return crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); } export async function encryptRegistrationReplay(input: { retryKey: string; pairingId: string; requestHash: string; response: T; }): Promise<{ ciphertext: string; nonce: string }> { const nonce = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv: nonce, additionalData: new TextEncoder().encode(`${input.pairingId}\0${input.requestHash}`), }, await registrationReplayKey(input.retryKey), new TextEncoder().encode(JSON.stringify(input.response)), ); return { ciphertext: bytesToBase64(new Uint8Array(ciphertext)), nonce: bytesToBase64(nonce), }; } export async function decryptRegistrationReplay(input: { retryKey: string; pairingId: string; requestHash: string; ciphertext: string; nonce: string; }): Promise { const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv: Uint8Array.from(base64ToBytes(input.nonce)).buffer, additionalData: new TextEncoder().encode(`${input.pairingId}\0${input.requestHash}`), }, await registrationReplayKey(input.retryKey), Uint8Array.from(base64ToBytes(input.ciphertext)).buffer, ); return JSON.parse(new TextDecoder().decode(plaintext)) as T; }