12345678910111213141516171819import { generateKeyPair, sharedKey } from "@stablelib/x25519";20import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305";21import { BLAKE2s } from "@stablelib/blake2s";22import { HMAC } from "@stablelib/hmac";2324const PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_BLAKE2s";2526export function initHash(): Uint8Array {27 const nameBytes = new TextEncoder().encode(PROTOCOL_NAME);28 if (nameBytes.length <= 32) {29 const h = new Uint8Array(32);30 h.set(nameBytes);31 return h;32 }33 34 35 36 return new BLAKE2s(32).update(nameBytes).digest();37}3839function mixHash(h: Uint8Array, data: Uint8Array): Uint8Array {40 const blake = new BLAKE2s(32);41 blake.update(h);42 blake.update(data);43 return blake.digest();44}4546export function hkdf2(ck: Uint8Array, ikm: Uint8Array): [Uint8Array, Uint8Array] {47 48 49 50 51 52 53 54 const prk = new HMAC(BLAKE2s, ck).update(ikm).digest(); 55 const k1 = new HMAC(BLAKE2s, prk).update(Uint8Array.of(1)).digest();56 const k2 = new HMAC(BLAKE2s, prk).update(k1).update(Uint8Array.of(2)).digest();57 return [k1, k2];58}5960function nonceBytes(n: bigint): Uint8Array {61 const buf = new Uint8Array(12);62 new DataView(buf.buffer).setBigUint64(4, n, true);63 return buf;64}6566export interface NoiseXXSession {67 encrypt(plaintext: Uint8Array): Uint8Array;68 decrypt(ciphertext: Uint8Array): Uint8Array;69 remoteStaticPublicKey: Uint8Array;70 handshakeHash: Uint8Array;71}72737475767778export async function noiseXXInitiator(79 ourStaticPrivateKey: Uint8Array,80 ourStaticPublicKey: Uint8Array,81 send: (msg: Uint8Array) => Promise<void>,82 receive: () => Promise<Uint8Array>,83 84 85 86 87 88 verifyRemoteStatic?: (remoteStaticPublicKey: Uint8Array) => void,89): Promise<NoiseXXSession> {90 let h: Uint8Array = initHash();91 let ck: Uint8Array = h.slice();9293 94 const e = generateKeyPair();95 h = mixHash(h, e.publicKey);96 97 98 99 h = mixHash(h, new Uint8Array(0));100 await send(e.publicKey);101102 103 const msg2 = await receive();104 if (msg2.length < 32 + 32 + 16 + 16) {105 throw new Error("Noise XX: msg2 too short");106 }107 const remoteEphemeral = msg2.slice(0, 32);108 const encryptedStatic = msg2.slice(32, 32 + 32 + 16);109 const encryptedPayload = msg2.slice(32 + 32 + 16);110111 h = mixHash(h, remoteEphemeral);112 113 114 const ee = sharedKey(e.secretKey, remoteEphemeral, true);115 const [ck1, k1] = hkdf2(ck, ee);116 ck = ck1;117118 const cipherStatic = new ChaCha20Poly1305(k1);119 const remoteStaticPlain = cipherStatic.open(nonceBytes(0n), encryptedStatic, h);120 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {121 throw new Error("Noise XX: msg2 static auth failed");122 }123 h = mixHash(h, encryptedStatic);124125 const es = sharedKey(e.secretKey, remoteStaticPlain, true);126 const [ck2, k2] = hkdf2(ck, es);127 ck = ck2;128129 const cipherPayload = new ChaCha20Poly1305(k2);130 const payloadPlain = cipherPayload.open(nonceBytes(0n), encryptedPayload, h);131 if (!payloadPlain) {132 throw new Error("Noise XX: msg2 payload auth failed");133 }134 h = mixHash(h, encryptedPayload);135136 137 138 139 if (verifyRemoteStatic) verifyRemoteStatic(remoteStaticPlain);140141 142 const cipherOurStatic = new ChaCha20Poly1305(k2);143 const encryptedOurStatic = cipherOurStatic.seal(nonceBytes(1n), ourStaticPublicKey, h);144 h = mixHash(h, encryptedOurStatic);145146 const se = sharedKey(ourStaticPrivateKey, remoteEphemeral, true);147 const [ck3, k3] = hkdf2(ck, se);148 ck = ck3;149150 const cipherFinal = new ChaCha20Poly1305(k3);151 const encryptedFinalPayload = cipherFinal.seal(nonceBytes(0n), new Uint8Array(0), h);152 h = mixHash(h, encryptedFinalPayload);153154 const msg3 = new Uint8Array(encryptedOurStatic.length + encryptedFinalPayload.length);155 msg3.set(encryptedOurStatic, 0);156 msg3.set(encryptedFinalPayload, encryptedOurStatic.length);157 await send(msg3);158159 160 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));161 const sendCipher = new ChaCha20Poly1305(initSend);162 const recvCipher = new ChaCha20Poly1305(respSend);163 let sendNonce = 0n;164 let recvNonce = 0n;165166 return {167 encrypt(plaintext) {168 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));169 },170 decrypt(ciphertext) {171 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));172 if (!result) throw new Error("Noise XX: decryption failed");173 return result;174 },175 remoteStaticPublicKey: remoteStaticPlain,176 handshakeHash: h.slice(),177 };178}179180181182183184export async function noiseXXResponder(185 ourStaticPrivateKey: Uint8Array,186 ourStaticPublicKey: Uint8Array,187 send: (msg: Uint8Array) => Promise<void>,188 receive: () => Promise<Uint8Array>,189): Promise<NoiseXXSession> {190 let h: Uint8Array = initHash();191 let ck: Uint8Array = h.slice();192193 194 const msg1 = await receive();195 if (msg1.length < 32) throw new Error("Noise XX: msg1 too short");196 const remoteEphemeral = msg1.slice(0, 32);197 h = mixHash(h, remoteEphemeral);198 199 h = mixHash(h, new Uint8Array(0));200201 202 const e = generateKeyPair();203 h = mixHash(h, e.publicKey);204 const ee = sharedKey(e.secretKey, remoteEphemeral, true);205 const [ck1, k1] = hkdf2(ck, ee);206 ck = ck1;207208 const cipherStatic = new ChaCha20Poly1305(k1);209 const encryptedOurStatic = cipherStatic.seal(nonceBytes(0n), ourStaticPublicKey, h);210 h = mixHash(h, encryptedOurStatic);211212 const es = sharedKey(ourStaticPrivateKey, remoteEphemeral, true);213 const [ck2, k2] = hkdf2(ck, es);214 ck = ck2;215216 const cipherPayload = new ChaCha20Poly1305(k2);217 const encryptedPayload = cipherPayload.seal(nonceBytes(0n), new Uint8Array(0), h);218 h = mixHash(h, encryptedPayload);219220 const msg2 = new Uint8Array(e.publicKey.length + encryptedOurStatic.length + encryptedPayload.length);221 msg2.set(e.publicKey, 0);222 msg2.set(encryptedOurStatic, e.publicKey.length);223 msg2.set(encryptedPayload, e.publicKey.length + encryptedOurStatic.length);224 await send(msg2);225226 227 const msg3 = await receive();228 if (msg3.length < 32 + 16 + 16) throw new Error("Noise XX: msg3 too short");229 const encryptedRemoteStatic = msg3.slice(0, 32 + 16);230 const encryptedFinalPayload = msg3.slice(32 + 16);231232 const cipherTheirStatic = new ChaCha20Poly1305(k2);233 const remoteStaticPlain = cipherTheirStatic.open(nonceBytes(1n), encryptedRemoteStatic, h);234 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {235 throw new Error("Noise XX: msg3 static auth failed");236 }237 h = mixHash(h, encryptedRemoteStatic);238239 const se = sharedKey(e.secretKey, remoteStaticPlain, true);240 const [ck3, k3] = hkdf2(ck, se);241 ck = ck3;242243 const cipherFinal = new ChaCha20Poly1305(k3);244 const finalPayloadPlain = cipherFinal.open(nonceBytes(0n), encryptedFinalPayload, h);245 if (!finalPayloadPlain) {246 throw new Error("Noise XX: msg3 final payload auth failed");247 }248 h = mixHash(h, encryptedFinalPayload);249250 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));251 const sendCipher = new ChaCha20Poly1305(respSend);252 const recvCipher = new ChaCha20Poly1305(initSend);253 let sendNonce = 0n;254 let recvNonce = 0n;255256 return {257 encrypt(plaintext) {258 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));259 },260 decrypt(ciphertext) {261 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));262 if (!result) throw new Error("Noise XX: decryption failed");263 return result;264 },265 remoteStaticPublicKey: remoteStaticPlain,266 handshakeHash: h.slice(),267 };268}