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 await send(e.publicKey);9798 99 const msg2 = await receive();100 if (msg2.length < 32 + 32 + 16 + 16) {101 throw new Error("Noise XX: msg2 too short");102 }103 const remoteEphemeral = msg2.slice(0, 32);104 const encryptedStatic = msg2.slice(32, 32 + 32 + 16);105 const encryptedPayload = msg2.slice(32 + 32 + 16);106107 h = mixHash(h, remoteEphemeral);108 109 110 const ee = sharedKey(e.secretKey, remoteEphemeral, true);111 const [ck1, k1] = hkdf2(ck, ee);112 ck = ck1;113114 const cipherStatic = new ChaCha20Poly1305(k1);115 const remoteStaticPlain = cipherStatic.open(nonceBytes(0n), encryptedStatic, h);116 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {117 throw new Error("Noise XX: msg2 static auth failed");118 }119 h = mixHash(h, encryptedStatic);120121 const es = sharedKey(e.secretKey, remoteStaticPlain, true);122 const [ck2, k2] = hkdf2(ck, es);123 ck = ck2;124125 const cipherPayload = new ChaCha20Poly1305(k2);126 const payloadPlain = cipherPayload.open(nonceBytes(0n), encryptedPayload, h);127 if (!payloadPlain) {128 throw new Error("Noise XX: msg2 payload auth failed");129 }130 h = mixHash(h, encryptedPayload);131132 133 134 135 if (verifyRemoteStatic) verifyRemoteStatic(remoteStaticPlain);136137 138 const cipherOurStatic = new ChaCha20Poly1305(k2);139 const encryptedOurStatic = cipherOurStatic.seal(nonceBytes(1n), ourStaticPublicKey, h);140 h = mixHash(h, encryptedOurStatic);141142 const se = sharedKey(ourStaticPrivateKey, remoteEphemeral, true);143 const [ck3, k3] = hkdf2(ck, se);144 ck = ck3;145146 const cipherFinal = new ChaCha20Poly1305(k3);147 const encryptedFinalPayload = cipherFinal.seal(nonceBytes(0n), new Uint8Array(0), h);148 h = mixHash(h, encryptedFinalPayload);149150 const msg3 = new Uint8Array(encryptedOurStatic.length + encryptedFinalPayload.length);151 msg3.set(encryptedOurStatic, 0);152 msg3.set(encryptedFinalPayload, encryptedOurStatic.length);153 await send(msg3);154155 156 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));157 const sendCipher = new ChaCha20Poly1305(initSend);158 const recvCipher = new ChaCha20Poly1305(respSend);159 let sendNonce = 0n;160 let recvNonce = 0n;161162 return {163 encrypt(plaintext) {164 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));165 },166 decrypt(ciphertext) {167 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));168 if (!result) throw new Error("Noise XX: decryption failed");169 return result;170 },171 remoteStaticPublicKey: remoteStaticPlain,172 handshakeHash: h.slice(),173 };174}175176177178179180export async function noiseXXResponder(181 ourStaticPrivateKey: Uint8Array,182 ourStaticPublicKey: Uint8Array,183 send: (msg: Uint8Array) => Promise<void>,184 receive: () => Promise<Uint8Array>,185): Promise<NoiseXXSession> {186 let h: Uint8Array = initHash();187 let ck: Uint8Array = h.slice();188189 190 const msg1 = await receive();191 if (msg1.length < 32) throw new Error("Noise XX: msg1 too short");192 const remoteEphemeral = msg1.slice(0, 32);193 h = mixHash(h, remoteEphemeral);194195 196 const e = generateKeyPair();197 h = mixHash(h, e.publicKey);198 const ee = sharedKey(e.secretKey, remoteEphemeral, true);199 const [ck1, k1] = hkdf2(ck, ee);200 ck = ck1;201202 const cipherStatic = new ChaCha20Poly1305(k1);203 const encryptedOurStatic = cipherStatic.seal(nonceBytes(0n), ourStaticPublicKey, h);204 h = mixHash(h, encryptedOurStatic);205206 const es = sharedKey(ourStaticPrivateKey, remoteEphemeral, true);207 const [ck2, k2] = hkdf2(ck, es);208 ck = ck2;209210 const cipherPayload = new ChaCha20Poly1305(k2);211 const encryptedPayload = cipherPayload.seal(nonceBytes(0n), new Uint8Array(0), h);212 h = mixHash(h, encryptedPayload);213214 const msg2 = new Uint8Array(e.publicKey.length + encryptedOurStatic.length + encryptedPayload.length);215 msg2.set(e.publicKey, 0);216 msg2.set(encryptedOurStatic, e.publicKey.length);217 msg2.set(encryptedPayload, e.publicKey.length + encryptedOurStatic.length);218 await send(msg2);219220 221 const msg3 = await receive();222 if (msg3.length < 32 + 16 + 16) throw new Error("Noise XX: msg3 too short");223 const encryptedRemoteStatic = msg3.slice(0, 32 + 16);224 const encryptedFinalPayload = msg3.slice(32 + 16);225226 const cipherTheirStatic = new ChaCha20Poly1305(k2);227 const remoteStaticPlain = cipherTheirStatic.open(nonceBytes(1n), encryptedRemoteStatic, h);228 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {229 throw new Error("Noise XX: msg3 static auth failed");230 }231 h = mixHash(h, encryptedRemoteStatic);232233 const se = sharedKey(e.secretKey, remoteStaticPlain, true);234 const [ck3, k3] = hkdf2(ck, se);235 ck = ck3;236237 const cipherFinal = new ChaCha20Poly1305(k3);238 const finalPayloadPlain = cipherFinal.open(nonceBytes(0n), encryptedFinalPayload, h);239 if (!finalPayloadPlain) {240 throw new Error("Noise XX: msg3 final payload auth failed");241 }242 h = mixHash(h, encryptedFinalPayload);243244 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));245 const sendCipher = new ChaCha20Poly1305(respSend);246 const recvCipher = new ChaCha20Poly1305(initSend);247 let sendNonce = 0n;248 let recvNonce = 0n;249250 return {251 encrypt(plaintext) {252 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));253 },254 decrypt(ciphertext) {255 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));256 if (!result) throw new Error("Noise XX: decryption failed");257 return result;258 },259 remoteStaticPublicKey: remoteStaticPlain,260 handshakeHash: h.slice(),261 };262}