Use message encryption with subscription API

This commit is contained in:
fryorcraken 2025-10-03 22:33:25 +10:00
parent 7fdd15e725
commit 066bc7fa7a
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
4 changed files with 200 additions and 3 deletions

View File

@ -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;
}

View File

@ -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";

View File

@ -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<Uint8Array> {
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<SymmetricDecryptionResult | undefined> {
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;
}
}
}

View File

@ -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
});
});
});
});