feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
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<boolean> {
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<string> {
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<DeviceIdentity> {
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<string> {
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";
}