export type DeviceIdentity = { ed25519Public: string; x25519Public: string; fingerprint: string; }; const PAIRING_ID = /^pair_[0-9a-f]{32}$/; const PAIRING_CODE = /^[0-9A-F]{20}$/; const PUBLIC_KEY = /^[A-Za-z0-9_-]{43}$/; const ED25519_SIGNATURE = /^[A-Za-z0-9_-]{86}$/; const HEX_64 = /^[0-9a-f]{64}$/; const REGISTRATION_PROOF_DOMAIN = new TextEncoder().encode( "nekonest-cloud/device-registration-proof/v1", ); function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); } function concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array { const result = new Uint8Array(left.length + right.length); result.set(left); result.set(right, left.length); return result; } function decodeBase64Url(value: string): Uint8Array { if (!PUBLIC_KEY.test(value)) throw new Error("invalid_public_key"); const base64 = `${value.replaceAll("-", "+").replaceAll("_", "/")}=`; let decoded: string; try { decoded = atob(base64); } catch { throw new Error("invalid_public_key"); } const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0)); if (bytes.length !== 32) throw new Error("invalid_public_key"); return bytes; } function decodeEd25519Signature(value: string): Uint8Array { if (!ED25519_SIGNATURE.test(value)) throw new Error("invalid_registration_proof"); const base64 = `${value.replaceAll("-", "+").replaceAll("_", "/")}==`; let decoded: string; try { decoded = atob(base64); } catch { throw new Error("invalid_registration_proof"); } const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0)); if (bytes.length !== 64) throw new Error("invalid_registration_proof"); return bytes; } function lengthPrefixed(value: string): Uint8Array { const encoded = new TextEncoder().encode(value); const result = new Uint8Array(4 + encoded.length); new DataView(result.buffer).setUint32(0, encoded.length, false); result.set(encoded, 4); return result; } export function deviceRegistrationProofTranscript(input: { bootstrapToken: string; os: string; ed25519Public: string; x25519Public: string; identityFingerprint: string; transportMode: string; }): Uint8Array { const fields = [ input.bootstrapToken.trim(), input.os.trim().toLowerCase(), input.ed25519Public.trim(), input.x25519Public.trim(), input.identityFingerprint.trim().toLowerCase(), input.transportMode.trim(), ].map(lengthPrefixed); const totalLength = fields.reduce( (total, field) => total + field.length, REGISTRATION_PROOF_DOMAIN.length, ); const transcript = new Uint8Array(totalLength); transcript.set(REGISTRATION_PROOF_DOMAIN); let offset = REGISTRATION_PROOF_DOMAIN.length; for (const field of fields) { transcript.set(field, offset); offset += field.length; } return transcript; } export async function verifyDeviceRegistrationProof(input: { bootstrapToken: string; os: string; ed25519Public: string; x25519Public: string; identityFingerprint: string; transportMode: string; registrationProof: string; }): Promise { try { const publicKey = decodeBase64Url(input.ed25519Public.trim()); const signature = decodeEd25519Signature(input.registrationProof.trim()); const key = await crypto.subtle.importKey( "raw", Uint8Array.from(publicKey).buffer, { name: "Ed25519" }, false, ["verify"], ); return crypto.subtle.verify( { name: "Ed25519" }, key, Uint8Array.from(signature).buffer, Uint8Array.from(deviceRegistrationProofTranscript(input)).buffer, ); } catch { return false; } } export function normalizePairingCode(value: string): string { const normalized = value.trim().toUpperCase(); if (!PAIRING_CODE.test(normalized)) throw new Error("invalid_pairing_code"); return normalized; } export function parseBootstrapToken(value: string): { pairingId: string; code: string; } { const trimmed = value.trim(); const separator = trimmed.indexOf("."); if (separator < 0 || separator !== trimmed.lastIndexOf(".")) { throw new Error("invalid_bootstrap_token"); } const pairingId = trimmed.slice(0, separator); if (!PAIRING_ID.test(pairingId)) throw new Error("invalid_bootstrap_token"); return { pairingId, code: normalizePairingCode(trimmed.slice(separator + 1)), }; } export async function sha256Hex(value: string | Uint8Array): Promise { const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value; const input = Uint8Array.from(bytes).buffer; return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", input))); } export function constantTimeEqualHex(left: string, right: string): boolean { const leftNormalized = left.toLowerCase(); const rightNormalized = right.toLowerCase(); let difference = leftNormalized.length ^ rightNormalized.length; const length = Math.max(leftNormalized.length, rightNormalized.length); for (let index = 0; index < length; index += 1) { difference |= (leftNormalized.charCodeAt(index) || 0) ^ (rightNormalized.charCodeAt(index) || 0); } return difference === 0; } export async function validateDeviceIdentity(input: { ed25519Public: string; x25519Public: string; identityFingerprint: string; }): Promise { const ed25519Public = input.ed25519Public.trim(); const x25519Public = input.x25519Public.trim(); const fingerprint = input.identityFingerprint.trim().toLowerCase(); const ed25519Bytes = decodeBase64Url(ed25519Public); const x25519Bytes = decodeBase64Url(x25519Public); if (!HEX_64.test(fingerprint)) throw new Error("invalid_identity_fingerprint"); const expected = await sha256Hex(concatBytes(ed25519Bytes, x25519Bytes)); if (!constantTimeEqualHex(expected, fingerprint)) { throw new Error("invalid_identity_fingerprint"); } return { ed25519Public, x25519Public, fingerprint }; } export function randomDeviceToken(): string { return bytesToHex(crypto.getRandomValues(new Uint8Array(32))); } export function rateWindowStart(now: Date): string { const windowMilliseconds = 10 * 60_000; return new Date( Math.floor(now.getTime() / windowMilliseconds) * windowMilliseconds, ).toISOString(); } export async function sourceFingerprint( rootSecret: string, source: string, ): Promise { const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(rootSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign( "HMAC", key, new TextEncoder().encode(`pairing-source-v1\0${source}`), ); return bytesToHex(new Uint8Array(signature)); } export function requestSource(request: Request, production: boolean): string { const edgeSource = request.headers.get("cf-connecting-ip")?.trim() ?? ""; if (/^[0-9A-Fa-f:.]{3,64}$/.test(edgeSource)) return edgeSource.toLowerCase(); if (production) throw new Error("trusted_source_unavailable"); return "local-development"; }