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 return rustNoiseKkResponder(122 ourStaticPrivateKey,123 ourStaticPublicKey,124 expectedInitiatorStaticPublicKey,125 send,126 receive,127 );128}129130131132133134135export async function noiseNKInitiator(136 responderStaticPublicKey: Uint8Array,137 send: (msg: Uint8Array) => Promise<void>,138 receive: () => Promise<Uint8Array>,139): Promise<NoiseNKSession> {140 if (isSmallOrderPubkey(responderStaticPublicKey)) {141 throw new Error("noise: small-order X25519 responder static key");142 }143 return rustNoiseNkInitiator(144 responderStaticPublicKey,145 send,146 receive,147 );148}149150151152153154155156157158159160161162163164165166const AUTH_DOMAIN = new TextEncoder().encode("peach-sync-v1-initiator-auth");167168export function computeAuthProof(169 initiatorPrivate: Uint8Array,170 initiatorPublic: Uint8Array,171 responderPublic: Uint8Array,172 nonce: Uint8Array,173 handshakeHash: Uint8Array,174): Uint8Array {175 if (nonce.length !== 32) throw new Error("auth nonce must be 32 bytes");176 if (handshakeHash.length !== 32) throw new Error("handshake hash must be 32 bytes");177 if (isSmallOrderPubkey(responderPublic)) {178 throw new Error("small-order X25519 responder public key");179 }180181 const dh = sharedKey(initiatorPrivate, responderPublic, true);182 const mac = new HMAC(SHA256, dh);183 184 185 186 mac.update(handshakeHash);187 mac.update(AUTH_DOMAIN);188 mac.update(initiatorPublic);189 mac.update(responderPublic);190 mac.update(nonce);191 return mac.digest();192}193194interface AuthChallengeFrame {195 type: "CHALLENGE";196 nonce_hex: string;197 responder_public_key_hex: string;198}199200interface AuthProofFrame {201 type: "PROOF";202 initiator_public_key_hex: string;203 proof_hex: string;204}205206function bytesToHex(bytes: Uint8Array): string {207 return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");208}209210function hexToBytes(hex: string): Uint8Array {211 if (hex.length % 2 !== 0) throw new Error("invalid hex");212 const bytes = new Uint8Array(hex.length / 2);213 for (let i = 0; i < hex.length; i += 2) {214 bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);215 }216 return bytes;217}218219220221222223224225226227228229230231232233export async function noiseNKResponder(234 ourStaticPrivateKey: Uint8Array,235 ourStaticPublicKey: Uint8Array,236 send: (msg: Uint8Array) => Promise<void>,237 receive: () => Promise<Uint8Array>,238): Promise<NoiseNKSession> {239 return rustNoiseNkResponder(240 ourStaticPrivateKey,241 ourStaticPublicKey,242 send,243 receive,244 );245}246247248249250251function constantTimeEq(a: Uint8Array, b: Uint8Array): boolean {252 if (a.length !== b.length) return false;253 let diff = 0;254 for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;255 return diff === 0;256}257258259260261262263264265266export async function performResponderAuth(267 session: NoiseNKSession,268 ourPrivateKey: Uint8Array,269 ourPublicKey: Uint8Array,270 sendEncryptedJson: (data: Uint8Array) => Promise<void>,271 receiveEncryptedJson: () => Promise<Uint8Array>,272): Promise<{ initiatorPublicKey: Uint8Array }> {273 274275 const nonce = new Uint8Array(32);276 crypto.getRandomValues(nonce);277278 const challenge: AuthChallengeFrame = {279 type: "CHALLENGE",280 nonce_hex: bytesToHex(nonce),281 responder_public_key_hex: bytesToHex(ourPublicKey),282 };283 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(challenge)));284285 const proofBytes = await receiveEncryptedJson();286 const proofFrame = JSON.parse(new TextDecoder().decode(proofBytes)) as AuthProofFrame;287 if (proofFrame.type !== "PROOF") {288 throw new Error("expected PROOF frame");289 }290291 const initiatorPublicKey = hexToBytes(proofFrame.initiator_public_key_hex);292 const claimedProof = hexToBytes(proofFrame.proof_hex);293 if (initiatorPublicKey.length !== 32) {294 throw new Error("initiator public key must be 32 bytes");295 }296 if (claimedProof.length !== 32) {297 throw new Error("proof must be 32 bytes");298 }299 if (isSmallOrderPubkey(initiatorPublicKey)) {300 throw new Error("small-order X25519 initiator public key");301 }302303 304 const dh = sharedKey(ourPrivateKey, initiatorPublicKey, true);305 const mac = new HMAC(SHA256, dh);306 307 308 mac.update(session.handshakeHash);309 mac.update(AUTH_DOMAIN);310 mac.update(initiatorPublicKey);311 mac.update(ourPublicKey);312 mac.update(nonce);313 const expected = mac.digest();314315 if (!constantTimeEq(expected, claimedProof)) {316 throw new Error("auth proof did not verify");317 }318 return { initiatorPublicKey };319}320321export async function performInitiatorAuth(322 session: NoiseNKSession,323 ourPrivateKey: Uint8Array,324 ourPublicKey: Uint8Array,325 responderPublicKey: Uint8Array,326 sendEncryptedJson: (data: Uint8Array) => Promise<void>,327 receiveEncryptedJson: () => Promise<Uint8Array>,328): Promise<void> {329 330 331332 const challengeBytes = await receiveEncryptedJson();333 const challenge = JSON.parse(new TextDecoder().decode(challengeBytes)) as AuthChallengeFrame;334 if (challenge.type !== "CHALLENGE") {335 throw new Error("expected CHALLENGE frame");336 }337 const nonce = hexToBytes(challenge.nonce_hex);338 const responderKeyFromChallenge = hexToBytes(challenge.responder_public_key_hex);339 340 341 342 if (343 responderKeyFromChallenge.length !== responderPublicKey.length ||344 !responderKeyFromChallenge.every((b, i) => b === responderPublicKey[i])345 ) {346 throw new Error("responder key in challenge does not match Noise handshake");347 }348349 const proof = computeAuthProof(350 ourPrivateKey,351 ourPublicKey,352 responderPublicKey,353 nonce,354 session.handshakeHash,355 );356 const proofFrame: AuthProofFrame = {357 type: "PROOF",358 initiator_public_key_hex: bytesToHex(ourPublicKey),359 proof_hex: bytesToHex(proof),360 };361 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(proofFrame)));362}363364export { bytesToHex, hexToBytes };