From 066bc7fa7a66f5cdf68a3cd3d5a187aeabc0eb1a Mon Sep 17 00:00:00 2001 From: fryorcraken Date: Fri, 3 Oct 2025 22:33:25 +1000 Subject: [PATCH] Use message encryption with subscription API --- .../message-encryption/src/crypto/utils.ts | 29 +++++ packages/message-encryption/src/index.ts | 8 +- packages/message-encryption/src/symmetric.ts | 102 ++++++++++++++++++ packages/tests/tests/waku.node.spec.ts | 64 ++++++++++- 4 files changed, 200 insertions(+), 3 deletions(-) diff --git a/packages/message-encryption/src/crypto/utils.ts b/packages/message-encryption/src/crypto/utils.ts index fd57811bd4..15e9ed4b08 100644 --- a/packages/message-encryption/src/crypto/utils.ts +++ b/packages/message-encryption/src/crypto/utils.ts @@ -74,3 +74,32 @@ export async function sign( export function keccak256(input: Uint8Array): Uint8Array { return new Uint8Array(sha3.keccak256.arrayBuffer(input)); } + +/** + * Compare two public keys, can be used to verify that a given signature matches + * expectations. + * + * @param publicKeyA - The first public key to compare + * @param publicKeyB - The second public key to compare + * @returns true if the public keys are the same + */ +export function comparePublicKeys( + publicKeyA: Uint8Array | undefined, + publicKeyB: Uint8Array | undefined +): boolean { + if (!publicKeyA || !publicKeyB) { + return false; + } + + if (publicKeyA.length !== publicKeyB.length) { + return false; + } + + for (let i = 0; i < publicKeyA.length; i++) { + if (publicKeyA[i] !== publicKeyB[i]) { + return false; + } + } + + return true; +} diff --git a/packages/message-encryption/src/index.ts b/packages/message-encryption/src/index.ts index 79b6753833..fdace3c8dc 100644 --- a/packages/message-encryption/src/index.ts +++ b/packages/message-encryption/src/index.ts @@ -1,11 +1,17 @@ import { + comparePublicKeys, generatePrivateKey, generateSymmetricKey, getPublicKey } from "./crypto/index.js"; import { DecodedMessage } from "./decoded_message.js"; -export { generatePrivateKey, generateSymmetricKey, getPublicKey }; +export { + generatePrivateKey, + generateSymmetricKey, + getPublicKey, + comparePublicKeys +}; export type { DecodedMessage }; export * as ecies from "./ecies.js"; diff --git a/packages/message-encryption/src/symmetric.ts b/packages/message-encryption/src/symmetric.ts index 87fcbbb8fc..2f44ef8606 100644 --- a/packages/message-encryption/src/symmetric.ts +++ b/packages/message-encryption/src/symmetric.ts @@ -206,3 +206,105 @@ export function createDecoder( ): Decoder { return new Decoder(contentTopic, routingInfo, symKey); } + +/** + * Result of decrypting a message with AES symmetric encryption. + */ +export interface SymmetricDecryptionResult { + /** The decrypted payload */ + payload: Uint8Array; + /** The signature if the message was signed */ + signature?: Uint8Array; + /** The recovered public key if the message was signed */ + signaturePublicKey?: Uint8Array; +} + +/** + * AES symmetric encryption. + * + * + * Follows [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/) encryption standard. + */ +export class SymmetricEncryption { + /** + * Creates an AES Symmetric encryption instance. + * + * @param symKey - The symmetric key for encryption (32 bytes recommended) + * @param sigPrivKey - Optional private key to sign messages before encryption + */ + public constructor( + private symKey: Uint8Array, + private sigPrivKey?: Uint8Array + ) {} + + /** + * Encrypts a byte array payload. + * + * The encryption process: + * 1. Optionally signs the payload with the private key + * 2. Adds padding to obscure payload size + * 3. Encrypts using AES-256-GCM + * + * @param payload - The data to encrypt + * @returns The encrypted payload + */ + public async encrypt(payload: Uint8Array): Promise { + const preparedPayload = await preCipher(payload, this.sigPrivKey); + return encryptSymmetric(preparedPayload, this.symKey); + } +} + +/** + * AES symmetric decryption. + * + * Follows [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/) encryption standard. + */ +export class SymmetricDecryption { + /** + * Creates an AES Symmetric decryption instance. + * + * @param symKey - The symmetric key for decryption (must match encryption key) + */ + public constructor(private symKey: Uint8Array) {} + + /** + * Decrypts an encrypted byte array payload. + * + * The decryption process: + * 1. Decrypts using AES-256-GCM + * 2. Removes padding + * 3. Verifies and recovers signature if present + * + * @param encryptedPayload - The encrypted data (from [[SymmetricEncryption.encrypt]]) + * @returns Object containing the decrypted payload and signature info, or undefined if decryption fails + */ + public async decrypt( + encryptedPayload: Uint8Array + ): Promise { + try { + const decryptedData = await decryptSymmetric( + encryptedPayload, + this.symKey + ); + + if (!decryptedData) { + return undefined; + } + + const result = postCipher(decryptedData); + + if (!result) { + return undefined; + } + + return { + payload: result.payload, + signature: result.sig?.signature, + signaturePublicKey: result.sig?.publicKey + }; + } catch (error) { + log.error("Failed to decrypt payload", error); + return undefined; + } + } +} diff --git a/packages/tests/tests/waku.node.spec.ts b/packages/tests/tests/waku.node.spec.ts index 7c07a8c5aa..7565de86a6 100644 --- a/packages/tests/tests/waku.node.spec.ts +++ b/packages/tests/tests/waku.node.spec.ts @@ -7,10 +7,17 @@ import type { RelayNode } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; -import { generateSymmetricKey } from "@waku/message-encryption"; +import { + comparePublicKeys, + generatePrivateKey, + generateSymmetricKey, + getPublicKey +} from "@waku/message-encryption"; import { createDecoder, - createEncoder + createEncoder, + SymmetricDecryption, + SymmetricDecryptionResult } from "@waku/message-encryption/symmetric"; import { createRelayNode } from "@waku/relay"; import { @@ -384,5 +391,58 @@ describe("Waku API", function () { expectedPubsubTopic: TestRoutingInfo.pubsubTopic }); }); + + it("Subscribe and receive messages encrypted with AES", async function () { + const symKey = generateSymmetricKey(); + const senderPrivKey = generatePrivateKey(); + // TODO: For now, still using encoder + const newEncoder = createEncoder({ + contentTopic: TestContentTopic, + routingInfo: TestRoutingInfo, + symKey, + sigPrivKey: senderPrivKey + }); + + // Setup payload decryption + const symDecryption = new SymmetricDecryption(symKey); + + // subscribe to second content topic + waku.messageEmitter.addEventListener(TestContentTopic, (event) => { + const encryptedPayload = event.detail; + void symDecryption + .decrypt(encryptedPayload) + .then((decryptionResult: SymmetricDecryptionResult | undefined) => { + if (!decryptionResult) return; + serviceNodes.messageCollector.callback({ + contentTopic: TestContentTopic, + payload: decryptionResult.payload + }); + + // TODO: probably best to adapt the message collector + expect(decryptionResult?.signature).to.not.be.undefined; + expect( + comparePublicKeys( + getPublicKey(senderPrivKey), + decryptionResult?.signaturePublicKey + ) + ); + // usually best to ignore decryption failure + }); + }); + await waku.subscribe([TestContentTopic]); + + await waku.lightPush.send(newEncoder, { + payload: utf8ToBytes(messageText) + }); + expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq( + true, + "Waiting for the message" + ); + serviceNodes.messageCollector.verifyReceivedMessage(1, { + expectedContentTopic: TestContentTopic, + expectedMessageText: messageText, + expectedPubsubTopic: TestRoutingInfo.pubsubTopic + }); + }); }); });