feat!: enable validation of `meta` field

This commit is contained in:
fryorcraken.eth 2023-03-13 14:09:59 +11:00
parent 702d3ddf34
commit 2dcd16f6f5
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
9 changed files with 394 additions and 17 deletions

View File

@ -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<TopicOnlyMessage> {

View File

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

View File

@ -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<DecodedMessage> {
constructor(public contentTopic: string) {}
constructor(
public contentTopic: string,
protected metaValidator?: IMetaValidator
) {}
fromWireToProtoObj(bytes: Uint8Array): Promise<IProtoMessage | undefined> {
const protoMessage = proto.WakuMessage.decode(bytes);
@ -149,7 +162,8 @@ export class Decoder implements IDecoder<DecodedMessage> {
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<DecodedMessage> {
* 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);
}

View File

@ -58,6 +58,13 @@ export interface IEncoder {
toProtoObj: (message: IMessage) => Promise<IProtoMessage | undefined>;
}
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<T extends IDecodedMessage> {

View File

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

View File

@ -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,116 @@ 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 = (
_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,
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;
}
)
);
});
});

View File

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

View File

@ -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,109 @@ 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 = (
_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, 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;
}
)
);
});
});

View File

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