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): Promise<NoiseXXSession> {75 let h: Uint8Array = initHash();76 let ck: Uint8Array = h.slice();7778 79 const e = generateKeyPair();80 h = mixHash(h, e.publicKey);81 await send(e.publicKey);8283 84 const msg2 = await receive();85 if (msg2.length < 32 + 32 + 16 + 16) {86 throw new Error("Noise XX: msg2 too short");87 }88 const remoteEphemeral = msg2.slice(0, 32);89 const encryptedStatic = msg2.slice(32, 32 + 32 + 16);90 const encryptedPayload = msg2.slice(32 + 32 + 16);9192 h = mixHash(h, remoteEphemeral);93 const ee = sharedKey(e.secretKey, remoteEphemeral);94 const [ck1, k1] = hkdf2(ck, ee);95 ck = ck1;9697 const cipherStatic = new ChaCha20Poly1305(k1);98 const remoteStaticPlain = cipherStatic.open(nonceBytes(0n), encryptedStatic, h);99 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {100 throw new Error("Noise XX: msg2 static auth failed");101 }102 h = mixHash(h, encryptedStatic);103104 const es = sharedKey(e.secretKey, remoteStaticPlain);105 const [ck2, k2] = hkdf2(ck, es);106 ck = ck2;107108 const cipherPayload = new ChaCha20Poly1305(k2);109 const payloadPlain = cipherPayload.open(nonceBytes(0n), encryptedPayload, h);110 if (!payloadPlain) {111 throw new Error("Noise XX: msg2 payload auth failed");112 }113 h = mixHash(h, encryptedPayload);114115 116 const cipherOurStatic = new ChaCha20Poly1305(k2);117 const encryptedOurStatic = cipherOurStatic.seal(nonceBytes(1n), ourStaticPublicKey, h);118 h = mixHash(h, encryptedOurStatic);119120 const se = sharedKey(ourStaticPrivateKey, remoteEphemeral);121 const [ck3, k3] = hkdf2(ck, se);122 ck = ck3;123124 const cipherFinal = new ChaCha20Poly1305(k3);125 const encryptedFinalPayload = cipherFinal.seal(nonceBytes(0n), new Uint8Array(0), h);126 h = mixHash(h, encryptedFinalPayload);127128 const msg3 = new Uint8Array(encryptedOurStatic.length + encryptedFinalPayload.length);129 msg3.set(encryptedOurStatic, 0);130 msg3.set(encryptedFinalPayload, encryptedOurStatic.length);131 await send(msg3);132133 134 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));135 const sendCipher = new ChaCha20Poly1305(initSend);136 const recvCipher = new ChaCha20Poly1305(respSend);137 let sendNonce = 0n;138 let recvNonce = 0n;139140 return {141 encrypt(plaintext) {142 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));143 },144 decrypt(ciphertext) {145 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));146 if (!result) throw new Error("Noise XX: decryption failed");147 return result;148 },149 remoteStaticPublicKey: remoteStaticPlain,150 handshakeHash: h.slice(),151 };152}153154155156157158export async function noiseXXResponder(159 ourStaticPrivateKey: Uint8Array,160 ourStaticPublicKey: Uint8Array,161 send: (msg: Uint8Array) => Promise<void>,162 receive: () => Promise<Uint8Array>,163): Promise<NoiseXXSession> {164 let h: Uint8Array = initHash();165 let ck: Uint8Array = h.slice();166167 168 const msg1 = await receive();169 if (msg1.length < 32) throw new Error("Noise XX: msg1 too short");170 const remoteEphemeral = msg1.slice(0, 32);171 h = mixHash(h, remoteEphemeral);172173 174 const e = generateKeyPair();175 h = mixHash(h, e.publicKey);176 const ee = sharedKey(e.secretKey, remoteEphemeral);177 const [ck1, k1] = hkdf2(ck, ee);178 ck = ck1;179180 const cipherStatic = new ChaCha20Poly1305(k1);181 const encryptedOurStatic = cipherStatic.seal(nonceBytes(0n), ourStaticPublicKey, h);182 h = mixHash(h, encryptedOurStatic);183184 const es = sharedKey(ourStaticPrivateKey, remoteEphemeral);185 const [ck2, k2] = hkdf2(ck, es);186 ck = ck2;187188 const cipherPayload = new ChaCha20Poly1305(k2);189 const encryptedPayload = cipherPayload.seal(nonceBytes(0n), new Uint8Array(0), h);190 h = mixHash(h, encryptedPayload);191192 const msg2 = new Uint8Array(e.publicKey.length + encryptedOurStatic.length + encryptedPayload.length);193 msg2.set(e.publicKey, 0);194 msg2.set(encryptedOurStatic, e.publicKey.length);195 msg2.set(encryptedPayload, e.publicKey.length + encryptedOurStatic.length);196 await send(msg2);197198 199 const msg3 = await receive();200 if (msg3.length < 32 + 16 + 16) throw new Error("Noise XX: msg3 too short");201 const encryptedRemoteStatic = msg3.slice(0, 32 + 16);202 const encryptedFinalPayload = msg3.slice(32 + 16);203204 const cipherTheirStatic = new ChaCha20Poly1305(k2);205 const remoteStaticPlain = cipherTheirStatic.open(nonceBytes(1n), encryptedRemoteStatic, h);206 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {207 throw new Error("Noise XX: msg3 static auth failed");208 }209 h = mixHash(h, encryptedRemoteStatic);210211 const se = sharedKey(e.secretKey, remoteStaticPlain);212 const [ck3, k3] = hkdf2(ck, se);213 ck = ck3;214215 const cipherFinal = new ChaCha20Poly1305(k3);216 const finalPayloadPlain = cipherFinal.open(nonceBytes(0n), encryptedFinalPayload, h);217 if (!finalPayloadPlain) {218 throw new Error("Noise XX: msg3 final payload auth failed");219 }220 h = mixHash(h, encryptedFinalPayload);221222 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));223 const sendCipher = new ChaCha20Poly1305(respSend);224 const recvCipher = new ChaCha20Poly1305(initSend);225 let sendNonce = 0n;226 let recvNonce = 0n;227228 return {229 encrypt(plaintext) {230 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));231 },232 decrypt(ciphertext) {233 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));234 if (!result) throw new Error("Noise XX: decryption failed");235 return result;236 },237 remoteStaticPublicKey: remoteStaticPlain,238 handshakeHash: h.slice(),239 };240}