mirror of https://github.com/waku-org/js-waku.git
feat!: enable validation of `meta` field
This commit is contained in:
parent
702d3ddf34
commit
2dcd16f6f5
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue