63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
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<CryptoKey> {
|
|
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<T>(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<T>(input: {
|
|
retryKey: string;
|
|
pairingId: string;
|
|
requestHash: string;
|
|
ciphertext: string;
|
|
nonce: string;
|
|
}): Promise<T> {
|
|
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;
|
|
}
|