noise.ts

6.5 KB · raw

1/**2 * Noise NK State Machine for Peach Sync3 *4 * Pattern: Noise NK5 * DH: X255196 * Cipher: ChaCha20-Poly13057 * Hash: BLAKE2s8 *9 * NK Pattern:10 *   <- s                    (pre-message: responder static public key)11 *   ...12 *   -> e, es                (initiator sends ephemeral key, performs DH with responder static)13 *   <- e, ee                (responder sends ephemeral key, performs DH)14 */1516import { 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}