12345678910111213141516171819import { generateKeyPair, sharedKey } from "@stablelib/x25519";20import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305";21import { BLAKE2s } from "@stablelib/blake2s";22import { HKDF } from "@stablelib/hkdf";23import { SHA256 } from "@stablelib/sha256";2425const PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_BLAKE2s";2627function initHash(): Uint8Array {28 const nameBytes = new TextEncoder().encode(PROTOCOL_NAME);29 if (nameBytes.length <= 32) {30 const h = new Uint8Array(32);31 h.set(nameBytes);32 return h;33 }34 const sha = new SHA256();35 sha.update(nameBytes);36 return sha.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}4546function hkdf2(ck: Uint8Array, ikm: Uint8Array): [Uint8Array, Uint8Array] {47 const out = new HKDF(SHA256, ikm, ck).expand(64);48 return [out.slice(0, 32), out.slice(32, 64)];49}5051function nonceBytes(n: bigint): Uint8Array {52 const buf = new Uint8Array(12);53 new DataView(buf.buffer).setBigUint64(4, n, true);54 return buf;55}5657export interface NoiseXXSession {58 encrypt(plaintext: Uint8Array): Uint8Array;59 decrypt(ciphertext: Uint8Array): Uint8Array;60 remoteStaticPublicKey: Uint8Array;61 handshakeHash: Uint8Array;62}63646566676869export async function noiseXXInitiator(70 ourStaticPrivateKey: Uint8Array,71 ourStaticPublicKey: Uint8Array,72 send: (msg: Uint8Array) => Promise<void>,73 receive: () => Promise<Uint8Array>,74 75 76 77 78 79 verifyRemoteStatic?: (remoteStaticPublicKey: Uint8Array) => void,80): Promise<NoiseXXSession> {81 let h: Uint8Array = initHash();82 let ck: Uint8Array = h.slice();8384 85 const e = generateKeyPair();86 h = mixHash(h, e.publicKey);87 await send(e.publicKey);8889 90 const msg2 = await receive();91 if (msg2.length < 32 + 32 + 16 + 16) {92 throw new Error("Noise XX: msg2 too short");93 }94 const remoteEphemeral = msg2.slice(0, 32);95 const encryptedStatic = msg2.slice(32, 32 + 32 + 16);96 const encryptedPayload = msg2.slice(32 + 32 + 16);9798 h = mixHash(h, remoteEphemeral);99 const ee = sharedKey(e.secretKey, remoteEphemeral);100 const [ck1, k1] = hkdf2(ck, ee);101 ck = ck1;102103 const cipherStatic = new ChaCha20Poly1305(k1);104 const remoteStaticPlain = cipherStatic.open(nonceBytes(0n), encryptedStatic, h);105 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {106 throw new Error("Noise XX: msg2 static auth failed");107 }108 h = mixHash(h, encryptedStatic);109110 const es = sharedKey(e.secretKey, remoteStaticPlain);111 const [ck2, k2] = hkdf2(ck, es);112 ck = ck2;113114 const cipherPayload = new ChaCha20Poly1305(k2);115 const payloadPlain = cipherPayload.open(nonceBytes(0n), encryptedPayload, h);116 if (!payloadPlain) {117 throw new Error("Noise XX: msg2 payload auth failed");118 }119 h = mixHash(h, encryptedPayload);120121 122 123 124 if (verifyRemoteStatic) verifyRemoteStatic(remoteStaticPlain);125126 127 const cipherOurStatic = new ChaCha20Poly1305(k2);128 const encryptedOurStatic = cipherOurStatic.seal(nonceBytes(1n), ourStaticPublicKey, h);129 h = mixHash(h, encryptedOurStatic);130131 const se = sharedKey(ourStaticPrivateKey, remoteEphemeral);132 const [ck3, k3] = hkdf2(ck, se);133 ck = ck3;134135 const cipherFinal = new ChaCha20Poly1305(k3);136 const encryptedFinalPayload = cipherFinal.seal(nonceBytes(0n), new Uint8Array(0), h);137 h = mixHash(h, encryptedFinalPayload);138139 const msg3 = new Uint8Array(encryptedOurStatic.length + encryptedFinalPayload.length);140 msg3.set(encryptedOurStatic, 0);141 msg3.set(encryptedFinalPayload, encryptedOurStatic.length);142 await send(msg3);143144 145 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));146 const sendCipher = new ChaCha20Poly1305(initSend);147 const recvCipher = new ChaCha20Poly1305(respSend);148 let sendNonce = 0n;149 let recvNonce = 0n;150151 return {152 encrypt(plaintext) {153 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));154 },155 decrypt(ciphertext) {156 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));157 if (!result) throw new Error("Noise XX: decryption failed");158 return result;159 },160 remoteStaticPublicKey: remoteStaticPlain,161 handshakeHash: h.slice(),162 };163}164165166167168169export async function noiseXXResponder(170 ourStaticPrivateKey: Uint8Array,171 ourStaticPublicKey: Uint8Array,172 send: (msg: Uint8Array) => Promise<void>,173 receive: () => Promise<Uint8Array>,174): Promise<NoiseXXSession> {175 let h: Uint8Array = initHash();176 let ck: Uint8Array = h.slice();177178 179 const msg1 = await receive();180 if (msg1.length < 32) throw new Error("Noise XX: msg1 too short");181 const remoteEphemeral = msg1.slice(0, 32);182 h = mixHash(h, remoteEphemeral);183184 185 const e = generateKeyPair();186 h = mixHash(h, e.publicKey);187 const ee = sharedKey(e.secretKey, remoteEphemeral);188 const [ck1, k1] = hkdf2(ck, ee);189 ck = ck1;190191 const cipherStatic = new ChaCha20Poly1305(k1);192 const encryptedOurStatic = cipherStatic.seal(nonceBytes(0n), ourStaticPublicKey, h);193 h = mixHash(h, encryptedOurStatic);194195 const es = sharedKey(ourStaticPrivateKey, remoteEphemeral);196 const [ck2, k2] = hkdf2(ck, es);197 ck = ck2;198199 const cipherPayload = new ChaCha20Poly1305(k2);200 const encryptedPayload = cipherPayload.seal(nonceBytes(0n), new Uint8Array(0), h);201 h = mixHash(h, encryptedPayload);202203 const msg2 = new Uint8Array(e.publicKey.length + encryptedOurStatic.length + encryptedPayload.length);204 msg2.set(e.publicKey, 0);205 msg2.set(encryptedOurStatic, e.publicKey.length);206 msg2.set(encryptedPayload, e.publicKey.length + encryptedOurStatic.length);207 await send(msg2);208209 210 const msg3 = await receive();211 if (msg3.length < 32 + 16 + 16) throw new Error("Noise XX: msg3 too short");212 const encryptedRemoteStatic = msg3.slice(0, 32 + 16);213 const encryptedFinalPayload = msg3.slice(32 + 16);214215 const cipherTheirStatic = new ChaCha20Poly1305(k2);216 const remoteStaticPlain = cipherTheirStatic.open(nonceBytes(1n), encryptedRemoteStatic, h);217 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {218 throw new Error("Noise XX: msg3 static auth failed");219 }220 h = mixHash(h, encryptedRemoteStatic);221222 const se = sharedKey(e.secretKey, remoteStaticPlain);223 const [ck3, k3] = hkdf2(ck, se);224 ck = ck3;225226 const cipherFinal = new ChaCha20Poly1305(k3);227 const finalPayloadPlain = cipherFinal.open(nonceBytes(0n), encryptedFinalPayload, h);228 if (!finalPayloadPlain) {229 throw new Error("Noise XX: msg3 final payload auth failed");230 }231 h = mixHash(h, encryptedFinalPayload);232233 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));234 const sendCipher = new ChaCha20Poly1305(respSend);235 const recvCipher = new ChaCha20Poly1305(initSend);236 let sendNonce = 0n;237 let recvNonce = 0n;238239 return {240 encrypt(plaintext) {241 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));242 },243 decrypt(ciphertext) {244 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));245 if (!result) throw new Error("Noise XX: decryption failed");246 return result;247 },248 remoteStaticPublicKey: remoteStaticPlain,249 handshakeHash: h.slice(),250 };251}