diff --git a/packages/core/src/lib/message/topic_only_message.ts b/packages/core/src/lib/message/topic_only_message.ts index fee30680aa..18e8ff96a1 100644 --- a/packages/core/src/lib/message/topic_only_message.ts +++ b/packages/core/src/lib/message/topic_only_message.ts @@ -23,6 +23,10 @@ export class TopicOnlyMessage implements IDecodedMessage { get contentTopic(): string { return this.proto.contentTopic; } + + isMetaValid(): boolean { + return true; + } } export class TopicOnlyDecoder implements IDecoder { diff --git a/packages/core/src/lib/message/version_0.spec.ts b/packages/core/src/lib/message/version_0.spec.ts index e1158fe0a9..275a45995a 100644 --- a/packages/core/src/lib/message/version_0.spec.ts +++ b/packages/core/src/lib/message/version_0.spec.ts @@ -105,4 +105,104 @@ describe("Waku Message version 0", function () { ) ); }); + + it("isMetaValid returns true when no validator specified", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + async (pubSubTopic, contentTopic, payload) => { + const encoder = createEncoder({ + contentTopic, + }); + const bytes = await encoder.toWire({ payload }); + const decoder = createDecoder(contentTopic); + const protoResult = await decoder.fromWireToProtoObj(bytes); + const result = (await decoder.fromProtoObj( + pubSubTopic, + protoResult! + )) as DecodedMessage; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); + + it("isMetaValid returns false when validator specified returns false", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + async (pubSubTopic, contentTopic, payload) => { + const encoder = createEncoder({ + contentTopic, + }); + const decoder = createDecoder(contentTopic, () => false); + + const bytes = await encoder.toWire({ payload }); + const protoResult = await decoder.fromWireToProtoObj(bytes); + const result = (await decoder.fromProtoObj( + pubSubTopic, + protoResult! + )) as DecodedMessage; + + expect(result.isMetaValid()).to.be.false; + } + ) + ); + }); + + it("isMetaValid returns true when matching meta setter", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + async (pubSubTopic, contentTopic, payload) => { + const metaSetter = ( + msg: IProtoMessage & { meta: undefined } + ): Uint8Array => { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, msg.payload.length); + return new Uint8Array(buffer); + }; + + const encoder = createEncoder({ + contentTopic, + metaSetter, + }); + + const metaValidator = ( + _pubSubTopic: string, + message: IProtoMessage + ): boolean => { + if (!message.meta) return false; + + const view = new DataView( + message.meta.buffer, + message.meta.byteOffset, + 4 + ); + const metaInt = view.getUint32(0); + + return metaInt === message.payload.length; + }; + const decoder = createDecoder(contentTopic, metaValidator); + + const bytes = await encoder.toWire({ payload }); + const protoResult = await decoder.fromWireToProtoObj(bytes); + const result = (await decoder.fromProtoObj( + pubSubTopic, + protoResult! + )) as DecodedMessage; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); }); diff --git a/packages/core/src/lib/message/version_0.ts b/packages/core/src/lib/message/version_0.ts index 0a5376eec0..3aa0d46720 100644 --- a/packages/core/src/lib/message/version_0.ts +++ b/packages/core/src/lib/message/version_0.ts @@ -1,4 +1,4 @@ -import { IMetaSetter } from "@waku/interfaces"; +import { IMetaSetter, IMetaValidator } from "@waku/interfaces"; import type { EncoderOptions, IDecodedMessage, @@ -11,6 +11,8 @@ import type { import { proto_message as proto } from "@waku/proto"; import debug from "debug"; +import { toProtoMessage } from "../to_proto_message.js"; + const log = debug("waku:message:version-0"); const OneMillion = BigInt(1_000_000); @@ -18,7 +20,11 @@ export const Version = 0; export { proto }; export class DecodedMessage implements IDecodedMessage { - constructor(public pubSubTopic: string, protected proto: proto.WakuMessage) {} + constructor( + public pubSubTopic: string, + protected proto: proto.WakuMessage, + private metaValidator: IMetaValidator + ) {} get ephemeral(): boolean { return Boolean(this.proto.ephemeral); @@ -64,6 +70,10 @@ export class DecodedMessage implements IDecodedMessage { get rateLimitProof(): IRateLimitProof | undefined { return this.proto.rateLimitProof; } + + isMetaValid(): boolean { + return this.metaValidator(this.pubSubTopic, toProtoMessage(this.proto)); + } } export class Encoder implements IEncoder { @@ -117,7 +127,10 @@ export function createEncoder({ } export class Decoder implements IDecoder { - constructor(public contentTopic: string) {} + constructor( + public contentTopic: string, + protected metaValidator?: IMetaValidator + ) {} fromWireToProtoObj(bytes: Uint8Array): Promise { const protoMessage = proto.WakuMessage.decode(bytes); @@ -149,7 +162,8 @@ export class Decoder implements IDecoder { return Promise.resolve(undefined); } - return new DecodedMessage(pubSubTopic, proto); + const metaValidator = this.metaValidator ?? (() => true); + return new DecodedMessage(pubSubTopic, proto, metaValidator); } } @@ -163,7 +177,11 @@ export class Decoder implements IDecoder { * messages. * * @param contentTopic The resulting decoder will only decode messages with this content topic. + * @param metaValidator Validator to use to verify meta field. */ -export function createDecoder(contentTopic: string): Decoder { - return new Decoder(contentTopic); +export function createDecoder( + contentTopic: string, + metaValidator?: IMetaValidator +): Decoder { + return new Decoder(contentTopic, metaValidator); } diff --git a/packages/interfaces/src/message.ts b/packages/interfaces/src/message.ts index c16d700109..af77f3aaab 100644 --- a/packages/interfaces/src/message.ts +++ b/packages/interfaces/src/message.ts @@ -58,6 +58,13 @@ export interface IEncoder { toProtoObj: (message: IMessage) => Promise; } +export interface IMetaValidator { + /** + * Used to validate the `meta` field of a message. + */ + (pubSubTopic: string, message: IProtoMessage): boolean; +} + export interface IDecodedMessage { payload: Uint8Array; contentTopic: string; @@ -65,6 +72,11 @@ export interface IDecodedMessage { timestamp: Date | undefined; rateLimitProof: IRateLimitProof | undefined; ephemeral: boolean | undefined; + /** + * Calls the { @link @waku/interface.message.IMetaValidator } passed on the + * decoder. Returns true if no meta validator is passed. + */ + isMetaValid: () => boolean; } export interface IDecoder { diff --git a/packages/message-encryption/src/decoded_message.ts b/packages/message-encryption/src/decoded_message.ts index 72e1626419..7aabbbe2f7 100644 --- a/packages/message-encryption/src/decoded_message.ts +++ b/packages/message-encryption/src/decoded_message.ts @@ -2,7 +2,7 @@ import { DecodedMessage as DecodedMessageV0, proto, } from "@waku/core/lib/message/version_0"; -import type { IDecodedMessage } from "@waku/interfaces"; +import type { IDecodedMessage, IMetaValidator } from "@waku/interfaces"; export class DecodedMessage extends DecodedMessageV0 @@ -14,10 +14,11 @@ export class DecodedMessage pubSubTopic: string, proto: proto.WakuMessage, decodedPayload: Uint8Array, + metaValidator: IMetaValidator, public signature?: Uint8Array, public signaturePublicKey?: Uint8Array ) { - super(pubSubTopic, proto); + super(pubSubTopic, proto, metaValidator); this._decodedPayload = decodedPayload; } diff --git a/packages/message-encryption/src/ecies.spec.ts b/packages/message-encryption/src/ecies.spec.ts index 698ecddeb0..20400c4c5a 100644 --- a/packages/message-encryption/src/ecies.spec.ts +++ b/packages/message-encryption/src/ecies.spec.ts @@ -1,3 +1,4 @@ +import { DecodedMessage } from "@waku/core"; import { IProtoMessage } from "@waku/interfaces"; import { expect } from "chai"; import fc from "fast-check"; @@ -129,4 +130,113 @@ describe("Ecies Encryption", function () { ) ); }); + + it("isMetaValid returns true when no meta validator is specified [ecies]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, privateKey) => { + const publicKey = getPublicKey(privateKey); + const encoder = createEncoder({ + contentTopic, + publicKey, + }); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(contentTopic, privateKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(pubSubTopic, protoResult); + if (!result) throw "Failed to decode"; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); + + it("isMetaValid returns false when validator specified returns false [ecies]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, privateKey) => { + const publicKey = getPublicKey(privateKey); + const encoder = createEncoder({ + contentTopic, + publicKey, + }); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(contentTopic, privateKey, () => false); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(pubSubTopic, protoResult); + if (!result) throw "Failed to decode"; + + expect(result.isMetaValid()).to.be.false; + } + ) + ); + }); + + it("isMetaValid returns true when matching meta setter [ecies]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, privateKey) => { + const publicKey = getPublicKey(privateKey); + const metaSetter = ( + msg: IProtoMessage & { meta: undefined } + ): Uint8Array => { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, msg.payload.length); + return new Uint8Array(buffer); + }; + + const encoder = createEncoder({ + contentTopic, + publicKey, + metaSetter, + }); + + const metaValidator = (message: IProtoMessage): boolean => { + if (!message.meta) return false; + + const view = new DataView( + message.meta.buffer, + message.meta.byteOffset, + 4 + ); + const metaInt = view.getUint32(0); + + return metaInt === message.payload.length; + }; + const decoder = createDecoder( + contentTopic, + privateKey, + metaValidator + ); + + const bytes = await encoder.toWire({ payload }); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + const result = (await decoder.fromProtoObj( + pubSubTopic, + protoResult! + )) as DecodedMessage; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); }); diff --git a/packages/message-encryption/src/ecies.ts b/packages/message-encryption/src/ecies.ts index 98d241e6e2..2b25ec7179 100644 --- a/packages/message-encryption/src/ecies.ts +++ b/packages/message-encryption/src/ecies.ts @@ -1,5 +1,5 @@ import { Decoder as DecoderV0 } from "@waku/core/lib/message/version_0"; -import { IMetaSetter } from "@waku/interfaces"; +import { IMetaSetter, IMetaValidator } from "@waku/interfaces"; import type { EncoderOptions as BaseEncoderOptions, IDecoder, @@ -107,8 +107,12 @@ export function createEncoder({ } class Decoder extends DecoderV0 implements IDecoder { - constructor(contentTopic: string, private privateKey: Uint8Array) { - super(contentTopic); + constructor( + contentTopic: string, + private privateKey: Uint8Array, + metaValidator?: IMetaValidator + ) { + super(contentTopic, metaValidator); } async fromProtoObj( @@ -152,10 +156,14 @@ class Decoder extends DecoderV0 implements IDecoder { } log("Message decrypted", protoMessage); + + const metaValidator = this.metaValidator ?? (() => true); + return new DecodedMessage( pubSubTopic, protoMessage, res.payload, + metaValidator, res.sig?.signature, res.sig?.publicKey ); @@ -174,10 +182,13 @@ class Decoder extends DecoderV0 implements IDecoder { * * @param contentTopic The resulting decoder will only decode messages with this content topic. * @param privateKey The private key used to decrypt the message. + * @param metaValidator function to validate the meta field. Available via + * { @link DecodedMessage.isMetaValid }. */ export function createDecoder( contentTopic: string, - privateKey: Uint8Array + privateKey: Uint8Array, + metaValidator?: IMetaValidator ): Decoder { - return new Decoder(contentTopic, privateKey); + return new Decoder(contentTopic, privateKey, metaValidator); } diff --git a/packages/message-encryption/src/symmetric.spec.ts b/packages/message-encryption/src/symmetric.spec.ts index cf724d2fa0..803b01f891 100644 --- a/packages/message-encryption/src/symmetric.spec.ts +++ b/packages/message-encryption/src/symmetric.spec.ts @@ -1,3 +1,4 @@ +import { DecodedMessage } from "@waku/core"; import { IProtoMessage } from "@waku/interfaces"; import { expect } from "chai"; import fc from "fast-check"; @@ -117,4 +118,106 @@ describe("Symmetric Encryption", function () { ) ); }); + + it("isMetaValid returns true when no meta validator is specified [symmetric]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, symKey) => { + const encoder = createEncoder({ + contentTopic, + symKey, + }); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(contentTopic, symKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(pubSubTopic, protoResult); + if (!result) throw "Failed to decode"; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); + + it("isMetaValid returns false when validator specified returns false [symmetric]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, symKey) => { + const encoder = createEncoder({ + contentTopic, + symKey, + }); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(contentTopic, symKey, () => false); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(pubSubTopic, protoResult); + if (!result) throw "Failed to decode"; + + expect(result.isMetaValid()).to.be.false; + } + ) + ); + }); + + it("isMetaValid returns true when matching meta setter [symmetric]", async function () { + await fc.assert( + fc.asyncProperty( + fc.string(), + fc.string(), + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (pubSubTopic, contentTopic, payload, symKey) => { + const metaSetter = ( + msg: IProtoMessage & { meta: undefined } + ): Uint8Array => { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, msg.payload.length); + return new Uint8Array(buffer); + }; + + const encoder = createEncoder({ + contentTopic, + symKey, + metaSetter, + }); + + const metaValidator = (message: IProtoMessage): boolean => { + if (!message.meta) return false; + + const view = new DataView( + message.meta.buffer, + message.meta.byteOffset, + 4 + ); + const metaInt = view.getUint32(0); + + return metaInt === message.payload.length; + }; + const decoder = createDecoder(contentTopic, symKey, metaValidator); + + const bytes = await encoder.toWire({ payload }); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + const result = (await decoder.fromProtoObj( + pubSubTopic, + protoResult! + )) as DecodedMessage; + + expect(result.isMetaValid()).to.be.true; + } + ) + ); + }); }); diff --git a/packages/message-encryption/src/symmetric.ts b/packages/message-encryption/src/symmetric.ts index 53bf05ee38..78ff56f144 100644 --- a/packages/message-encryption/src/symmetric.ts +++ b/packages/message-encryption/src/symmetric.ts @@ -1,4 +1,5 @@ import { Decoder as DecoderV0 } from "@waku/core/lib/message/version_0"; +import { IMetaValidator } from "@waku/interfaces"; import type { EncoderOptions as BaseEncoderOptions, IDecoder, @@ -97,8 +98,12 @@ export function createEncoder({ } class Decoder extends DecoderV0 implements IDecoder { - constructor(contentTopic: string, private symKey: Uint8Array) { - super(contentTopic); + constructor( + contentTopic: string, + private symKey: Uint8Array, + metaValidator?: IMetaValidator + ) { + super(contentTopic, metaValidator); } async fromProtoObj( @@ -142,10 +147,14 @@ class Decoder extends DecoderV0 implements IDecoder { } log("Message decrypted", protoMessage); + + const metaValidator = this.metaValidator ?? (() => true); + return new DecodedMessage( pubSubTopic, protoMessage, res.payload, + metaValidator, res.sig?.signature, res.sig?.publicKey ); @@ -164,10 +173,13 @@ class Decoder extends DecoderV0 implements IDecoder { * * @param contentTopic The resulting decoder will only decode messages with this content topic. * @param symKey The symmetric key used to decrypt the message. + * @param metaValidator function to validate the meta field. Available via + * { @link DecodedMessage.isMetaValid }. */ export function createDecoder( contentTopic: string, - symKey: Uint8Array + symKey: Uint8Array, + metaValidator?: IMetaValidator ): Decoder { - return new Decoder(contentTopic, symKey); + return new Decoder(contentTopic, symKey, metaValidator); }