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 const ee = sharedKey(e.secretKey, remoteEphemeral);109 const [ck1, k1] = hkdf2(ck, ee);110 ck = ck1;111112 const cipherStatic = new ChaCha20Poly1305(k1);113 const remoteStaticPlain = cipherStatic.open(nonceBytes(0n), encryptedStatic, h);114 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {115 throw new Error("Noise XX: msg2 static auth failed");116 }117 h = mixHash(h, encryptedStatic);118119 const es = sharedKey(e.secretKey, remoteStaticPlain);120 const [ck2, k2] = hkdf2(ck, es);121 ck = ck2;122123 const cipherPayload = new ChaCha20Poly1305(k2);124 const payloadPlain = cipherPayload.open(nonceBytes(0n), encryptedPayload, h);125 if (!payloadPlain) {126 throw new Error("Noise XX: msg2 payload auth failed");127 }128 h = mixHash(h, encryptedPayload);129130 131 132 133 if (verifyRemoteStatic) verifyRemoteStatic(remoteStaticPlain);134135 136 const cipherOurStatic = new ChaCha20Poly1305(k2);137 const encryptedOurStatic = cipherOurStatic.seal(nonceBytes(1n), ourStaticPublicKey, h);138 h = mixHash(h, encryptedOurStatic);139140 const se = sharedKey(ourStaticPrivateKey, remoteEphemeral);141 const [ck3, k3] = hkdf2(ck, se);142 ck = ck3;143144 const cipherFinal = new ChaCha20Poly1305(k3);145 const encryptedFinalPayload = cipherFinal.seal(nonceBytes(0n), new Uint8Array(0), h);146 h = mixHash(h, encryptedFinalPayload);147148 const msg3 = new Uint8Array(encryptedOurStatic.length + encryptedFinalPayload.length);149 msg3.set(encryptedOurStatic, 0);150 msg3.set(encryptedFinalPayload, encryptedOurStatic.length);151 await send(msg3);152153 154 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));155 const sendCipher = new ChaCha20Poly1305(initSend);156 const recvCipher = new ChaCha20Poly1305(respSend);157 let sendNonce = 0n;158 let recvNonce = 0n;159160 return {161 encrypt(plaintext) {162 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));163 },164 decrypt(ciphertext) {165 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));166 if (!result) throw new Error("Noise XX: decryption failed");167 return result;168 },169 remoteStaticPublicKey: remoteStaticPlain,170 handshakeHash: h.slice(),171 };172}173174175176177178export async function noiseXXResponder(179 ourStaticPrivateKey: Uint8Array,180 ourStaticPublicKey: Uint8Array,181 send: (msg: Uint8Array) => Promise<void>,182 receive: () => Promise<Uint8Array>,183): Promise<NoiseXXSession> {184 let h: Uint8Array = initHash();185 let ck: Uint8Array = h.slice();186187 188 const msg1 = await receive();189 if (msg1.length < 32) throw new Error("Noise XX: msg1 too short");190 const remoteEphemeral = msg1.slice(0, 32);191 h = mixHash(h, remoteEphemeral);192193 194 const e = generateKeyPair();195 h = mixHash(h, e.publicKey);196 const ee = sharedKey(e.secretKey, remoteEphemeral);197 const [ck1, k1] = hkdf2(ck, ee);198 ck = ck1;199200 const cipherStatic = new ChaCha20Poly1305(k1);201 const encryptedOurStatic = cipherStatic.seal(nonceBytes(0n), ourStaticPublicKey, h);202 h = mixHash(h, encryptedOurStatic);203204 const es = sharedKey(ourStaticPrivateKey, remoteEphemeral);205 const [ck2, k2] = hkdf2(ck, es);206 ck = ck2;207208 const cipherPayload = new ChaCha20Poly1305(k2);209 const encryptedPayload = cipherPayload.seal(nonceBytes(0n), new Uint8Array(0), h);210 h = mixHash(h, encryptedPayload);211212 const msg2 = new Uint8Array(e.publicKey.length + encryptedOurStatic.length + encryptedPayload.length);213 msg2.set(e.publicKey, 0);214 msg2.set(encryptedOurStatic, e.publicKey.length);215 msg2.set(encryptedPayload, e.publicKey.length + encryptedOurStatic.length);216 await send(msg2);217218 219 const msg3 = await receive();220 if (msg3.length < 32 + 16 + 16) throw new Error("Noise XX: msg3 too short");221 const encryptedRemoteStatic = msg3.slice(0, 32 + 16);222 const encryptedFinalPayload = msg3.slice(32 + 16);223224 const cipherTheirStatic = new ChaCha20Poly1305(k2);225 const remoteStaticPlain = cipherTheirStatic.open(nonceBytes(1n), encryptedRemoteStatic, h);226 if (!remoteStaticPlain || remoteStaticPlain.length !== 32) {227 throw new Error("Noise XX: msg3 static auth failed");228 }229 h = mixHash(h, encryptedRemoteStatic);230231 const se = sharedKey(e.secretKey, remoteStaticPlain);232 const [ck3, k3] = hkdf2(ck, se);233 ck = ck3;234235 const cipherFinal = new ChaCha20Poly1305(k3);236 const finalPayloadPlain = cipherFinal.open(nonceBytes(0n), encryptedFinalPayload, h);237 if (!finalPayloadPlain) {238 throw new Error("Noise XX: msg3 final payload auth failed");239 }240 h = mixHash(h, encryptedFinalPayload);241242 const [initSend, respSend] = hkdf2(ck, new Uint8Array(0));243 const sendCipher = new ChaCha20Poly1305(respSend);244 const recvCipher = new ChaCha20Poly1305(initSend);245 let sendNonce = 0n;246 let recvNonce = 0n;247248 return {249 encrypt(plaintext) {250 return sendCipher.seal(nonceBytes(sendNonce++), plaintext, new Uint8Array(0));251 },252 decrypt(ciphertext) {253 const result = recvCipher.open(nonceBytes(recvNonce++), ciphertext, new Uint8Array(0));254 if (!result) throw new Error("Noise XX: decryption failed");255 return result;256 },257 remoteStaticPublicKey: remoteStaticPlain,258 handshakeHash: h.slice(),259 };260}