12345678910111213141516import { x25519 } from "@noble/curves/ed25519.js";17import { chacha20poly1305 } from "@noble/ciphers/chacha.js";18import { blake2s } from "@noble/hashes/blake2.js";19import { randomBytes } from "@noble/hashes/utils.js";2021export interface KeyPair {22 publicKey: Uint8Array;23 privateKey: Uint8Array;24}2526export interface NoiseResult {27 output: Uint8Array | null;28 canSend: boolean;29 isComplete: boolean;30}3132export class NoiseNKStateMachine {33 private staticKeyPair: KeyPair;34 private ephemeralKeyPair: KeyPair | null = null;35 private remoteEphemeralKey: Uint8Array | null = null;36 private readonly isInitiator: boolean;37 private remoteStaticKey: Uint8Array | null = null;38 private sendNonce = 0;39 private recvNonce = 0;40 private sessionKey: Uint8Array | null = null;41 private handshakeComplete = false;42 private canSendMessages = false;4344 constructor(staticKeyPair: KeyPair, isInitiator: boolean) {45 this.staticKeyPair = staticKeyPair;46 this.isInitiator = isInitiator;47 }4849 setRemoteStaticKey(key: Uint8Array): void {50 if (this.isInitiator) {51 this.remoteStaticKey = key;52 }53 }5455 private deriveSessionKey(dhOutputs: Uint8Array[]): Uint8Array {56 const totalLength = dhOutputs.reduce((sum, arr) => sum + arr.length, 0);57 const concat = new Uint8Array(totalLength);58 let offset = 0;59 for (const output of dhOutputs) {60 concat.set(output, offset);61 offset += output.length;62 }63 return blake2s(concat, { dkLen: 32 });64 }6566 private performDH(localPrivate: Uint8Array, remotePublic: Uint8Array): Uint8Array {67 return x25519.getSharedSecret(localPrivate, remotePublic);68 }6970 private generateEphemeralKeyPair(): KeyPair {71 const privateKey = randomBytes(32);72 const publicKey = x25519.getPublicKey(privateKey);73 return { publicKey, privateKey };74 }7576 private encryptWithKey(key: Uint8Array, plaintext: Uint8Array, nonce: bigint): Uint8Array {77 const nonceBytes = new Uint8Array(12);78 const view = new DataView(nonceBytes.buffer);79 view.setBigUint64(4, nonce, true);80 const cipher = chacha20poly1305(key, nonceBytes);81 return cipher.encrypt(plaintext);82 }8384 private decryptWithKey(key: Uint8Array, ciphertext: Uint8Array, nonce: bigint): Uint8Array {85 const nonceBytes = new Uint8Array(12);86 const view = new DataView(nonceBytes.buffer);87 view.setBigUint64(4, nonce, true);88 const cipher = chacha20poly1305(key, nonceBytes);89 return cipher.decrypt(ciphertext);90 }9192 readMessage(data: Uint8Array): NoiseResult {93 if (this.handshakeComplete) {94 return { output: null, canSend: this.canSendMessages, isComplete: true };95 }9697 if (this.isInitiator) {98 if (!this.remoteStaticKey) {99 throw new Error("Remote static key not set");100 }101102 if (this.ephemeralKeyPair === null) {103 this.ephemeralKeyPair = this.generateEphemeralKeyPair();104 const e = this.ephemeralKeyPair.publicKey;105 const es = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteStaticKey);106 const key = this.deriveSessionKey([es]);107 const output = this.encryptWithKey(key, data, BigInt(this.sendNonce));108 this.sendNonce++;109 return { output, canSend: true, isComplete: false };110 } else {111 this.remoteEphemeralKey = data;112 const ee = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteEphemeralKey);113 this.sessionKey = this.deriveSessionKey([ee]);114 this.handshakeComplete = true;115 this.canSendMessages = true;116 return { output: null, canSend: true, isComplete: true };117 }118 } else {119 if (this.remoteEphemeralKey === null) {120 this.remoteEphemeralKey = data;121 const e = this.staticKeyPair.publicKey;122 const ee = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);123 this.sessionKey = this.deriveSessionKey([ee]);124 return { output: e, canSend: true, isComplete: false };125 } else {126 const es = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);127 const key = this.deriveSessionKey([es]);128 const output = this.decryptWithKey(key, data, BigInt(this.recvNonce));129 this.recvNonce++;130 this.sessionKey = this.deriveSessionKey([es, this.remoteEphemeralKey]);131 this.handshakeComplete = true;132 this.canSendMessages = true;133 return { output, canSend: true, isComplete: true };134 }135 }136 }137138 writeMessage(payload: Uint8Array): Uint8Array {139 if (this.handshakeComplete) {140 return this.encrypt(payload);141 }142143 if (this.isInitiator) {144 if (this.ephemeralKeyPair === null) {145 this.ephemeralKeyPair = this.generateEphemeralKeyPair();146 return this.ephemeralKeyPair.publicKey;147 } else if (this.remoteEphemeralKey === null) {148 if (!this.remoteStaticKey) {149 throw new Error("Remote static key not set");150 }151 const es = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteStaticKey);152 const key = this.deriveSessionKey([es]);153 return this.encryptWithKey(key, payload, BigInt(this.sendNonce));154 }155 throw new Error("Invalid handshake state");156 } else {157 if (this.remoteEphemeralKey === null) {158 this.remoteEphemeralKey = payload;159 return this.staticKeyPair.publicKey;160 } else {161 const es = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);162 const key = this.deriveSessionKey([es]);163 this.sessionKey = key;164 this.handshakeComplete = true;165 this.canSendMessages = true;166 return this.encryptWithKey(key, payload, BigInt(this.sendNonce));167 }168 }169 }170171 getSessionKey(): Uint8Array | null {172 return this.sessionKey;173 }174175 encrypt(plaintext: Uint8Array): Uint8Array {176 if (!this.sessionKey) {177 throw new Error("Session key not available");178 }179 const ciphertext = this.encryptWithKey(this.sessionKey, plaintext, BigInt(this.sendNonce));180 this.sendNonce++;181 return ciphertext;182 }183184 decrypt(ciphertext: Uint8Array): Uint8Array {185 if (!this.sessionKey) {186 throw new Error("Session key not available");187 }188 const plaintext = this.decryptWithKey(this.sessionKey, ciphertext, BigInt(this.recvNonce));189 this.recvNonce++;190 return plaintext;191 }192}