import { ecb } from '@noble/ciphers/aes.js'; import { hmac } from '@noble/hashes/hmac.js'; import { sha256 } from "@noble/hashes/sha2.js"; import { BaseGroup, BaseGroupSecret, DecryptedGroupData, DecryptedGroupText, EncryptedPayload } from "./packet.types"; import { BufferReader, equalBytes, hexToBytes } from "./parser"; // The "Public" group is a special group that all nodes are implicitly part of. It uses a fixed secret derived from the string "Public". const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72"); export class Group extends BaseGroup {} export class GroupSecret extends BaseGroupSecret { public static fromName(name: string): GroupSecret { if (name === "Public") { return new GroupSecret(publicSecret); } else if (!/^#/.test(name)) { throw new Error("Only the 'Public' group or groups starting with '#' are supported"); } const hash = sha256.create().update(new TextEncoder().encode(name)).digest(); return new GroupSecret(hash.slice(0, 16)); } public decryptText(encrypted: EncryptedPayload): DecryptedGroupText { const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText); if (data.length < 8) { throw new Error("Invalid ciphertext"); } const reader = new BufferReader(data); const timestamp = reader.readTimestamp(); const flags = reader.readByte(); const textType = (flags >> 2) & 0x3F; const attempt = flags & 0x03; const message = new TextDecoder('utf-8').decode(reader.readBytes()); return { timestamp, textType, attempt, message } } public decryptData(encrypted: EncryptedPayload): DecryptedGroupData { const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText); if (data.length < 8) { throw new Error("Invalid ciphertext"); } const reader = new BufferReader(data); return { timestamp: reader.readTimestamp(), data: reader.readBytes(reader.remainingBytes()) }; } private macThenDecrypt(cipherMAC: Uint8Array, cipherText: Uint8Array): Uint8Array { const mac = hmac(sha256, this.secret, cipherText); if (!equalBytes(mac, cipherMAC)) { throw new Error("Invalid MAC"); } const block = ecb(this.secret.slice(0, 16), { disablePadding: true }); const plain = block.decrypt(cipherText); return plain; } }