12345678910import { sharedKey } from "@stablelib/x25519";11import { BLAKE2s } from "@stablelib/blake2s";12import { SHA256 } from "@stablelib/sha256";13import { HMAC } from "@stablelib/hmac";14import {15 rustNoiseKkInitiator,16 rustNoiseKkResponder,17 rustNoiseNkInitiator,18 rustNoiseNkResponder,19} from "./noise-core";2021const PROTOCOL_NAME = "Noise_NK_25519_ChaChaPoly_BLAKE2s";2223242526const X25519_SMALL_ORDER_PUBKEYS = new Set([27 "00".repeat(32),28 "01" + "00".repeat(31),29 "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",30 "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",31 "ec" + "ff".repeat(30) + "7f",32 "ed" + "ff".repeat(30) + "7f",33 "ee" + "ff".repeat(30) + "7f",34 "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",35 "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",36]);3738export function isSmallOrderPubkey(publicKey: Uint8Array): boolean {39 return publicKey.length === 32 && X25519_SMALL_ORDER_PUBKEYS.has(bytesToHex(publicKey));40}4142function initProtocolHash(protocolName: string): Uint8Array {43 const nameBytes = new TextEncoder().encode(protocolName);44 if (nameBytes.length <= 32) {45 const h = new Uint8Array(32);46 h.set(nameBytes);47 return h;48 }49 50 51 return new BLAKE2s(32).update(nameBytes).digest();52}5354export function initHash(): Uint8Array {55 return initProtocolHash(PROTOCOL_NAME);56}5758export function hkdf2(ck: Uint8Array, ikm: Uint8Array): [Uint8Array, Uint8Array] {59 60 61 62 63 64 65 66 const prk = new HMAC(BLAKE2s, ck).update(ikm).digest(); 67 const k1 = new HMAC(BLAKE2s, prk).update(Uint8Array.of(1)).digest();68 const k2 = new HMAC(BLAKE2s, prk).update(k1).update(Uint8Array.of(2)).digest();69 return [k1, k2];70}7172export interface NoiseNKSession {73 encrypt(plaintext: Uint8Array): Uint8Array;74 decrypt(ciphertext: Uint8Array): Uint8Array;75 readonly handshakeHash: Uint8Array;76 rekeyOutgoing(): void;77 rekeyIncoming(): void;78 close(): void;79}808182838485868788899091export async function noiseKKInitiator(92 ourStaticPrivateKey: Uint8Array,93 ourStaticPublicKey: Uint8Array,94 responderStaticPublicKey: Uint8Array,95 send: (msg: Uint8Array) => Promise<void>,96 receive: () => Promise<Uint8Array>,97): Promise<NoiseNKSession> {98 if (isSmallOrderPubkey(responderStaticPublicKey)) {99 throw new Error("noise: small-order X25519 responder static key");100 }101 return rustNoiseKkInitiator(102 ourStaticPrivateKey,103 ourStaticPublicKey,104 responderStaticPublicKey,105 send,106 receive,107 );108}109110111112113114export async function noiseKKResponder(115 ourStaticPrivateKey: Uint8Array,116 ourStaticPublicKey: Uint8Array,117 expectedInitiatorStaticPublicKey: Uint8Array,118 send: (msg: Uint8Array) => Promise<void>,119 receive: () => Promise<Uint8Array>,120): Promise<NoiseNKSession> {121 if (isSmallOrderPubkey(expectedInitiatorStaticPublicKey)) {122 throw new Error("noise: small-order X25519 initiator static key");123 }124 return rustNoiseKkResponder(125 ourStaticPrivateKey,126 ourStaticPublicKey,127 expectedInitiatorStaticPublicKey,128 send,129 receive,130 );131}132133134135136137138export async function noiseNKInitiator(139 responderStaticPublicKey: Uint8Array,140 send: (msg: Uint8Array) => Promise<void>,141 receive: () => Promise<Uint8Array>,142): Promise<NoiseNKSession> {143 if (isSmallOrderPubkey(responderStaticPublicKey)) {144 throw new Error("noise: small-order X25519 responder static key");145 }146 return rustNoiseNkInitiator(147 responderStaticPublicKey,148 send,149 receive,150 );151}152153154155156157158159160161162163164165166167168169const AUTH_DOMAIN = new TextEncoder().encode("peach-sync-v1-initiator-auth");170171export function computeAuthProof(172 initiatorPrivate: Uint8Array,173 initiatorPublic: Uint8Array,174 responderPublic: Uint8Array,175 nonce: Uint8Array,176 handshakeHash: Uint8Array,177): Uint8Array {178 if (nonce.length !== 32) throw new Error("auth nonce must be 32 bytes");179 if (handshakeHash.length !== 32) throw new Error("handshake hash must be 32 bytes");180 if (isSmallOrderPubkey(responderPublic)) {181 throw new Error("small-order X25519 responder public key");182 }183184 const dh = sharedKey(initiatorPrivate, responderPublic, true);185 const mac = new HMAC(SHA256, dh);186 187 188 189 mac.update(handshakeHash);190 mac.update(AUTH_DOMAIN);191 mac.update(initiatorPublic);192 mac.update(responderPublic);193 mac.update(nonce);194 return mac.digest();195}196197interface AuthChallengeFrame {198 type: "CHALLENGE";199 nonce_hex: string;200 responder_public_key_hex: string;201}202203interface AuthProofFrame {204 type: "PROOF";205 initiator_public_key_hex: string;206 proof_hex: string;207}208209function bytesToHex(bytes: Uint8Array): string {210 return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");211}212213function hexToBytes(hex: string): Uint8Array {214 if (hex.length % 2 !== 0) throw new Error("invalid hex");215 const bytes = new Uint8Array(hex.length / 2);216 for (let i = 0; i < hex.length; i += 2) {217 bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);218 }219 return bytes;220}221222223224225226227228229230231232233234235236export async function noiseNKResponder(237 ourStaticPrivateKey: Uint8Array,238 ourStaticPublicKey: Uint8Array,239 send: (msg: Uint8Array) => Promise<void>,240 receive: () => Promise<Uint8Array>,241): Promise<NoiseNKSession> {242 return rustNoiseNkResponder(243 ourStaticPrivateKey,244 ourStaticPublicKey,245 send,246 receive,247 );248}249250251252253254function constantTimeEq(a: Uint8Array, b: Uint8Array): boolean {255 if (a.length !== b.length) return false;256 let diff = 0;257 for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;258 return diff === 0;259}260261262263264265266267268269export async function performResponderAuth(270 session: NoiseNKSession,271 ourPrivateKey: Uint8Array,272 ourPublicKey: Uint8Array,273 sendEncryptedJson: (data: Uint8Array) => Promise<void>,274 receiveEncryptedJson: () => Promise<Uint8Array>,275): Promise<{ initiatorPublicKey: Uint8Array }> {276 277278 const nonce = new Uint8Array(32);279 crypto.getRandomValues(nonce);280281 const challenge: AuthChallengeFrame = {282 type: "CHALLENGE",283 nonce_hex: bytesToHex(nonce),284 responder_public_key_hex: bytesToHex(ourPublicKey),285 };286 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(challenge)));287288 const proofBytes = await receiveEncryptedJson();289 const proofFrame = JSON.parse(new TextDecoder().decode(proofBytes)) as AuthProofFrame;290 if (proofFrame.type !== "PROOF") {291 throw new Error("expected PROOF frame");292 }293294 const initiatorPublicKey = hexToBytes(proofFrame.initiator_public_key_hex);295 const claimedProof = hexToBytes(proofFrame.proof_hex);296 if (initiatorPublicKey.length !== 32) {297 throw new Error("initiator public key must be 32 bytes");298 }299 if (claimedProof.length !== 32) {300 throw new Error("proof must be 32 bytes");301 }302 if (isSmallOrderPubkey(initiatorPublicKey)) {303 throw new Error("small-order X25519 initiator public key");304 }305306 307 const dh = sharedKey(ourPrivateKey, initiatorPublicKey, true);308 const mac = new HMAC(SHA256, dh);309 310 311 mac.update(session.handshakeHash);312 mac.update(AUTH_DOMAIN);313 mac.update(initiatorPublicKey);314 mac.update(ourPublicKey);315 mac.update(nonce);316 const expected = mac.digest();317318 if (!constantTimeEq(expected, claimedProof)) {319 throw new Error("auth proof did not verify");320 }321 return { initiatorPublicKey };322}323324export async function performInitiatorAuth(325 session: NoiseNKSession,326 ourPrivateKey: Uint8Array,327 ourPublicKey: Uint8Array,328 responderPublicKey: Uint8Array,329 sendEncryptedJson: (data: Uint8Array) => Promise<void>,330 receiveEncryptedJson: () => Promise<Uint8Array>,331): Promise<void> {332 333 334335 const challengeBytes = await receiveEncryptedJson();336 const challenge = JSON.parse(new TextDecoder().decode(challengeBytes)) as AuthChallengeFrame;337 if (challenge.type !== "CHALLENGE") {338 throw new Error("expected CHALLENGE frame");339 }340 const nonce = hexToBytes(challenge.nonce_hex);341 const responderKeyFromChallenge = hexToBytes(challenge.responder_public_key_hex);342 343 344 345 if (346 responderKeyFromChallenge.length !== responderPublicKey.length ||347 !responderKeyFromChallenge.every((b, i) => b === responderPublicKey[i])348 ) {349 throw new Error("responder key in challenge does not match Noise handshake");350 }351352 const proof = computeAuthProof(353 ourPrivateKey,354 ourPublicKey,355 responderPublicKey,356 nonce,357 session.handshakeHash,358 );359 const proofFrame: AuthProofFrame = {360 type: "PROOF",361 initiator_public_key_hex: bytesToHex(ourPublicKey),362 proof_hex: bytesToHex(proof),363 };364 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(proofFrame)));365}366367export { bytesToHex, hexToBytes };