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";20import { constantTimeEqualBytes } from "./constant-time";2122const PROTOCOL_NAME = "Noise_NK_25519_ChaChaPoly_BLAKE2s";2324252627const X25519_SMALL_ORDER_PUBKEYS = new Set([28 "00".repeat(32),29 "01" + "00".repeat(31),30 "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",31 "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",32 "ec" + "ff".repeat(30) + "7f",33 "ed" + "ff".repeat(30) + "7f",34 "ee" + "ff".repeat(30) + "7f",35 "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",36 "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",37]);3839export function isSmallOrderPubkey(publicKey: Uint8Array): boolean {40 return publicKey.length === 32 && X25519_SMALL_ORDER_PUBKEYS.has(bytesToHex(publicKey));41}4243function initProtocolHash(protocolName: string): Uint8Array {44 const nameBytes = new TextEncoder().encode(protocolName);45 if (nameBytes.length <= 32) {46 const h = new Uint8Array(32);47 h.set(nameBytes);48 return h;49 }50 51 52 return new BLAKE2s(32).update(nameBytes).digest();53}5455export function initHash(): Uint8Array {56 return initProtocolHash(PROTOCOL_NAME);57}5859export function hkdf2(ck: Uint8Array, ikm: Uint8Array): [Uint8Array, Uint8Array] {60 61 62 63 64 65 66 67 const prk = new HMAC(BLAKE2s, ck).update(ikm).digest(); 68 const k1 = new HMAC(BLAKE2s, prk).update(Uint8Array.of(1)).digest();69 const k2 = new HMAC(BLAKE2s, prk).update(k1).update(Uint8Array.of(2)).digest();70 return [k1, k2];71}7273export interface NoiseNKSession {74 encrypt(plaintext: Uint8Array): Uint8Array;75 decrypt(ciphertext: Uint8Array): Uint8Array;76 readonly handshakeHash: Uint8Array;77 rekeyOutgoing(): void;78 rekeyIncoming(): void;79 close(): void;80}818283848586878889909192export async function noiseKKInitiator(93 ourStaticPrivateKey: Uint8Array,94 ourStaticPublicKey: Uint8Array,95 responderStaticPublicKey: Uint8Array,96 send: (msg: Uint8Array) => Promise<void>,97 receive: () => Promise<Uint8Array>,98): Promise<NoiseNKSession> {99 if (isSmallOrderPubkey(responderStaticPublicKey)) {100 throw new Error("noise: small-order X25519 responder static key");101 }102 return rustNoiseKkInitiator(103 ourStaticPrivateKey,104 ourStaticPublicKey,105 responderStaticPublicKey,106 send,107 receive,108 );109}110111112113114115export async function noiseKKResponder(116 ourStaticPrivateKey: Uint8Array,117 ourStaticPublicKey: Uint8Array,118 expectedInitiatorStaticPublicKey: Uint8Array,119 send: (msg: Uint8Array) => Promise<void>,120 receive: () => Promise<Uint8Array>,121): Promise<NoiseNKSession> {122 if (isSmallOrderPubkey(expectedInitiatorStaticPublicKey)) {123 throw new Error("noise: small-order X25519 initiator static key");124 }125 return rustNoiseKkResponder(126 ourStaticPrivateKey,127 ourStaticPublicKey,128 expectedInitiatorStaticPublicKey,129 send,130 receive,131 );132}133134135136137138139export async function noiseNKInitiator(140 responderStaticPublicKey: Uint8Array,141 send: (msg: Uint8Array) => Promise<void>,142 receive: () => Promise<Uint8Array>,143): Promise<NoiseNKSession> {144 if (isSmallOrderPubkey(responderStaticPublicKey)) {145 throw new Error("noise: small-order X25519 responder static key");146 }147 return rustNoiseNkInitiator(148 responderStaticPublicKey,149 send,150 receive,151 );152}153154155156157158159160161162163164165166167168169170const AUTH_DOMAIN = new TextEncoder().encode("peach-sync-v1-initiator-auth");171172export function computeAuthProof(173 initiatorPrivate: Uint8Array,174 initiatorPublic: Uint8Array,175 responderPublic: Uint8Array,176 nonce: Uint8Array,177 handshakeHash: Uint8Array,178): Uint8Array {179 if (nonce.length !== 32) throw new Error("auth nonce must be 32 bytes");180 if (handshakeHash.length !== 32) throw new Error("handshake hash must be 32 bytes");181 if (isSmallOrderPubkey(responderPublic)) {182 throw new Error("small-order X25519 responder public key");183 }184185 const dh = sharedKey(initiatorPrivate, responderPublic, true);186 const mac = new HMAC(SHA256, dh);187 188 189 190 mac.update(handshakeHash);191 mac.update(AUTH_DOMAIN);192 mac.update(initiatorPublic);193 mac.update(responderPublic);194 mac.update(nonce);195 return mac.digest();196}197198interface AuthChallengeFrame {199 type: "CHALLENGE";200 nonce_hex: string;201 responder_public_key_hex: string;202}203204interface AuthProofFrame {205 type: "PROOF";206 initiator_public_key_hex: string;207 proof_hex: string;208}209210function bytesToHex(bytes: Uint8Array): string {211 return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");212}213214function hexToBytes(hex: string): Uint8Array {215 if (hex.length % 2 !== 0) throw new Error("invalid hex");216 const bytes = new Uint8Array(hex.length / 2);217 for (let i = 0; i < hex.length; i += 2) {218 bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);219 }220 return bytes;221}222223224225226227228229230231232233234235236237export async function noiseNKResponder(238 ourStaticPrivateKey: Uint8Array,239 ourStaticPublicKey: Uint8Array,240 send: (msg: Uint8Array) => Promise<void>,241 receive: () => Promise<Uint8Array>,242): Promise<NoiseNKSession> {243 return rustNoiseNkResponder(244 ourStaticPrivateKey,245 ourStaticPublicKey,246 send,247 receive,248 );249}250251252253254255256257258259export async function performResponderAuth(260 session: NoiseNKSession,261 ourPrivateKey: Uint8Array,262 ourPublicKey: Uint8Array,263 sendEncryptedJson: (data: Uint8Array) => Promise<void>,264 receiveEncryptedJson: () => Promise<Uint8Array>,265): Promise<{ initiatorPublicKey: Uint8Array }> {266 267268 const nonce = new Uint8Array(32);269 crypto.getRandomValues(nonce);270271 const challenge: AuthChallengeFrame = {272 type: "CHALLENGE",273 nonce_hex: bytesToHex(nonce),274 responder_public_key_hex: bytesToHex(ourPublicKey),275 };276 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(challenge)));277278 const proofBytes = await receiveEncryptedJson();279 const proofFrame = JSON.parse(new TextDecoder().decode(proofBytes)) as AuthProofFrame;280 if (proofFrame.type !== "PROOF") {281 throw new Error("expected PROOF frame");282 }283284 const initiatorPublicKey = hexToBytes(proofFrame.initiator_public_key_hex);285 const claimedProof = hexToBytes(proofFrame.proof_hex);286 if (initiatorPublicKey.length !== 32) {287 throw new Error("initiator public key must be 32 bytes");288 }289 if (claimedProof.length !== 32) {290 throw new Error("proof must be 32 bytes");291 }292 if (isSmallOrderPubkey(initiatorPublicKey)) {293 throw new Error("small-order X25519 initiator public key");294 }295296 297 const dh = sharedKey(ourPrivateKey, initiatorPublicKey, true);298 const mac = new HMAC(SHA256, dh);299 300 301 mac.update(session.handshakeHash);302 mac.update(AUTH_DOMAIN);303 mac.update(initiatorPublicKey);304 mac.update(ourPublicKey);305 mac.update(nonce);306 const expected = mac.digest();307308 if (!constantTimeEqualBytes(expected, claimedProof)) {309 throw new Error("auth proof did not verify");310 }311 return { initiatorPublicKey };312}313314export async function performInitiatorAuth(315 session: NoiseNKSession,316 ourPrivateKey: Uint8Array,317 ourPublicKey: Uint8Array,318 responderPublicKey: Uint8Array,319 sendEncryptedJson: (data: Uint8Array) => Promise<void>,320 receiveEncryptedJson: () => Promise<Uint8Array>,321): Promise<void> {322 323 324325 const challengeBytes = await receiveEncryptedJson();326 const challenge = JSON.parse(new TextDecoder().decode(challengeBytes)) as AuthChallengeFrame;327 if (challenge.type !== "CHALLENGE") {328 throw new Error("expected CHALLENGE frame");329 }330 const nonce = hexToBytes(challenge.nonce_hex);331 const responderKeyFromChallenge = hexToBytes(challenge.responder_public_key_hex);332 333 334 335 if (!constantTimeEqualBytes(responderKeyFromChallenge, responderPublicKey)) {336 throw new Error("responder key in challenge does not match Noise handshake");337 }338339 const proof = computeAuthProof(340 ourPrivateKey,341 ourPublicKey,342 responderPublicKey,343 nonce,344 session.handshakeHash,345 );346 const proofFrame: AuthProofFrame = {347 type: "PROOF",348 initiator_public_key_hex: bytesToHex(ourPublicKey),349 proof_hex: bytesToHex(proof),350 };351 await sendEncryptedJson(new TextEncoder().encode(JSON.stringify(proofFrame)));352}353354export { bytesToHex, hexToBytes };