Reduce Buffer usage in ENR module (#522)

This commit is contained in:
Franck R 2022-02-16 12:11:54 +11:00 committed by GitHub
parent 9931011c93
commit 297d65ce03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1129 additions and 255 deletions

View File

@ -1,6 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": ["ts-node/register", "isomorphic-fetch"],
"require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
"exit": true
}

View File

@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- axios dependency in favour of fetch.
- `axios` dependency in favour of fetch.
- `base64url` and `bigint-buffer` dependencies.
## [0.16.0] - 2022-01-31

889
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,15 +51,13 @@
"doc:cname": "echo 'js-waku.wakuconnect.dev' > build/docs/CNAME",
"doc:examples": "mkdir -p build/docs/examples",
"deploy": "node ci/deploy.js",
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build && for d in examples/*; do (cd $d; npm i); done"
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build && for d in examples/*/; do (cd $d; npm i); done"
},
"engines": {
"node": ">=16"
},
"dependencies": {
"@chainsafe/libp2p-noise": "^5.0.0",
"base64url": "^3.0.1",
"bigint-buffer": "^1.1.5",
"debug": "^4.3.1",
"dns-query": "^0.8.0",
"ecies-geth": "^1.5.2",
@ -107,6 +105,8 @@
"fast-check": "^2.14.0",
"gh-pages": "^3.2.3",
"isomorphic-fetch": "^3.0.0",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12",
"karma-chrome-launcher": "^3.1.0",
"karma-env-preprocessor": "^0.1.1",

View File

@ -3,6 +3,8 @@ export * as discovery from "./lib/discovery";
export * as enr from "./lib/enr";
export * as utf8 from "./lib/utf8";
export * as utils from "./lib/utils";
export * as waku from "./lib/waku";

View File

@ -183,10 +183,7 @@ describe("DNS Node Discovery [live data]", function () {
const maxQuantity = 3;
before(function () {
if (
process.env.CI ||
(typeof window !== "undefined" && window?.__env__?.CI)
) {
if (process.env.CI || window?.__env__?.CI) {
this.skip();
}
});

View File

@ -1,11 +1,10 @@
import assert from "assert";
import base64url from "base64url";
import * as base32 from "hi-base32";
import { ecdsaVerify } from "secp256k1";
import { ENR } from "../enr";
import { keccak256Buf } from "../utils";
import { base64ToBytes, keccak256Buf } from "../utils";
export type ENRRootValues = {
eRoot: string;
@ -43,15 +42,12 @@ export class ENRTree {
// (Trailing recovery bit must be trimmed to pass `ecdsaVerify` method)
const signedComponent = root.split(" sig")[0];
const signedComponentBuffer = Buffer.from(signedComponent);
const signatureBuffer = base64url
.toBuffer(rootValues.signature)
.slice(0, 64);
const keyBuffer = Buffer.from(decodedPublicKey);
const signatureBuffer = base64ToBytes(rootValues.signature).slice(0, 64);
const isVerified = ecdsaVerify(
signatureBuffer,
keccak256Buf(signedComponentBuffer),
keyBuffer
new Uint8Array(decodedPublicKey)
);
assert(isVerified, "Unable to verify ENRTree root signature");

View File

@ -45,10 +45,7 @@ describe("Discovery", () => {
describe("Discovery [live data]", function () {
before(function () {
if (
process.env.CI ||
(typeof window !== "undefined" && window?.__env__?.CI)
) {
if (process.env.CI || window?.__env__?.CI) {
this.skip();
}
});

View File

@ -2,9 +2,9 @@ import { bytesToHex } from "../utils";
import { NodeId } from "./types";
export function createNodeId(buffer: Buffer): NodeId {
if (buffer.length !== 32) {
export function createNodeId(bytes: Uint8Array): NodeId {
if (bytes.length !== 32) {
throw new Error("NodeId must be 32 bytes in length");
}
return bytesToHex(buffer);
return bytesToHex(bytes);
}

View File

@ -28,8 +28,8 @@ describe("ENR", function () {
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
),
];
const txt = enr.encodeTxt(keypair.privateKey);
expect(txt.slice(0, 4)).to.be.equal("enr:");
const txt = await enr.encodeTxt(keypair.privateKey);
const enr2 = ENR.decodeTxt(txt);
expect(bytesToHex(enr2.signature as Buffer)).to.be.equal(
bytesToHex(enr.signature as Buffer)
@ -87,17 +87,21 @@ describe("ENR", function () {
expect(enr.ip).to.not.be.undefined;
expect(enr.ip).to.be.equal("134.209.139.210");
expect(enr.publicKey).to.not.be.undefined;
expect(enr.peerId.toB58String()).to.be.equal(
expect(enr.peerId?.toB58String()).to.be.equal(
"16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ"
);
});
it("should throw error - no id", () => {
it("should throw error - no id", async () => {
try {
const txt = Buffer.from(
"656e723a2d435972595a62404b574342526c4179357a7a61445a584a42476b636e68344d486342465a6e75584e467264764a6a5830346a527a6a7a",
"hex"
).toString();
const peerId = await PeerId.create({ keyType: "secp256k1" });
const enr = ENR.createFromPeerId(peerId);
const keypair = createKeypairFromPeerId(peerId);
enr.setLocationMultiaddr(new Multiaddr("/ip4/18.223.219.100/udp/9000"));
enr.set("id", new Uint8Array([0]));
const txt = await enr.encodeTxt(keypair.privateKey);
ENR.decodeTxt(txt);
assert.fail("Expect error here");
} catch (err: unknown) {
@ -170,37 +174,6 @@ describe("ENR", function () {
});
});
describe("Fuzzing testcases", () => {
it("should throw error in invalid signature", () => {
const buf = Buffer.from(
"656e723a2d4b7634514147774f54385374716d7749354c486149796d494f346f6f464b664e6b456a576130663150384f73456c67426832496a622d4772445f2d623957346b6350466377796e354845516d526371584e716470566f3168656f42683246306447356c64484f494141414141414141414143455a58526f4d704141414141414141414141505f5f5f5f5f5f5f5f5f5f676d6c6b676e5930676d6c7768424c663232534a6332566a634449314e6d73786f514a78436e4536765f7832656b67595f756f45317274777a76477934306d7139654436365866485042576749494e315a48437f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f434436410d0a",
"hex"
).toString();
try {
ENR.decodeTxt(buf);
} catch (err: unknown) {
const e = err as Error;
expect(e.message).to.equal(
"Decoded ENR invalid signature: must be a byte array"
);
}
});
it("should throw error in invalid sequence number", () => {
const buf = Buffer.from(
"656e723a2d495334514b6b33ff583945717841337838334162436979416e537550444d764b353264433530486d31584744643574457951684d3356634a4c2d5062446b44673541507a5f706f76763022d48dcf992d5379716b306e616e636f4e572d656e7263713042676d6c6b676e5930676d6c77684838414141474a6332566a634449314e6d73786f514d31453579557370397638516a397476335a575843766146427672504e647a384b5049314e68576651577a494e315a4843434239410a",
"hex"
).toString();
try {
ENR.decodeTxt(buf);
} catch (err: unknown) {
const e = err as Error;
expect(e.message).to.equal(
"Decoded ENR invalid sequence number: must be a byte array"
);
}
});
});
describe("Static tests", () => {
let privateKey: Buffer;
let record: ENR;
@ -212,9 +185,10 @@ describe("ENR", function () {
"hex"
);
record = ENR.createV4(v4.publicKey(privateKey));
record.set("ip", Buffer.from("7f000001", "hex"));
record.set("udp", Buffer.from((30303).toString(16), "hex"));
record.setLocationMultiaddr(new Multiaddr("/ip4/127.0.0.1/udp/30303"));
record.seq = seq;
// To set signature
record.encode(privateKey);
});
it("should properly compute the node id", () => {
@ -228,7 +202,7 @@ describe("ENR", function () {
expect(decoded).to.deep.equal(record);
});
it("should encode/decode to text encoding", () => {
it("should encode/decode to text encoding", async () => {
// spec enr https://eips.ethereum.org/EIPS/eip-778
const testTxt =
"enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
@ -236,7 +210,8 @@ describe("ENR", function () {
expect(decoded.udp).to.be.equal(30303);
expect(decoded.ip).to.be.equal("127.0.0.1");
expect(decoded).to.deep.equal(record);
expect(record.encodeTxt(privateKey)).to.equal(testTxt);
const recordTxt = await record.encodeTxt(privateKey);
expect(recordTxt).to.equal(testTxt);
});
});

View File

@ -1,5 +1,3 @@
import base64url from "base64url";
import { toBigIntBE } from "bigint-buffer";
import { Multiaddr, protocols } from "multiaddr";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
@ -8,12 +6,10 @@ import PeerId from "peer-id";
import * as RLP from "rlp";
import { encode as varintEncode } from "varint";
import {
ERR_INVALID_ID,
ERR_NO_SIGNATURE,
MAX_RECORD_SIZE,
MULTIADDR_LENGTH_SIZE,
} from "./constants";
import { bytesToUtf8, utf8ToBytes } from "../utf8";
import { base64ToBytes, bytesToBase64, bytesToHex } from "../utils";
import { ERR_INVALID_ID, ERR_NO_SIGNATURE, MAX_RECORD_SIZE } from "./constants";
import {
createKeypair,
createKeypairFromPeerId,
@ -21,28 +17,32 @@ import {
IKeypair,
KeypairType,
} from "./keypair";
import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec";
import { ENRKey, ENRValue, NodeId, SequenceNumber } from "./types";
import * as v4 from "./v4";
export class ENR extends Map<ENRKey, ENRValue> {
public static readonly RECORD_PREFIX = "enr:";
public seq: SequenceNumber;
public signature: Buffer | null;
public signature: Uint8Array | null;
constructor(
kvs: Record<ENRKey, ENRValue> = {},
seq: SequenceNumber = 1n,
signature: Buffer | null = null
signature: Uint8Array | null = null
) {
super(Object.entries(kvs));
this.seq = seq;
this.signature = signature;
}
static createV4(publicKey: Buffer, kvs: Record<ENRKey, ENRValue> = {}): ENR {
static createV4(
publicKey: Uint8Array,
kvs: Record<ENRKey, ENRValue> = {}
): ENR {
return new ENR({
...kvs,
id: Buffer.from("v4"),
id: utf8ToBytes("v4"),
secp256k1: publicKey,
});
}
@ -78,9 +78,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
}
const obj: Record<ENRKey, ENRValue> = {};
for (let i = 0; i < kvs.length; i += 2) {
obj[kvs[i].toString()] = Buffer.from(kvs[i + 1]);
obj[kvs[i].toString()] = new Uint8Array(kvs[i + 1]);
}
const enr = new ENR(obj, toBigIntBE(seq), signature);
const enr = new ENR(
obj,
BigInt("0x" + bytesToHex(seq)),
new Uint8Array(signature)
);
if (!enr.verify(RLP.encode([seq, ...kvs]), signature)) {
throw new Error("Unable to verify ENR signature");
@ -88,7 +92,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return enr;
}
static decode(encoded: Buffer): ENR {
static decode(encoded: Uint8Array): ENR {
const decoded = RLP.decode(encoded) as unknown as Buffer[];
return ENR.decodeFromValues(decoded);
}
@ -99,7 +103,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
`"string encoded ENR must start with '${this.RECORD_PREFIX}'`
);
}
return ENR.decode(base64url.toBuffer(encoded.slice(4)));
return ENR.decode(base64ToBytes(encoded.slice(4)));
}
set(k: ENRKey, v: ENRValue): this {
@ -109,9 +113,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
}
get id(): string {
const id = this.get("id") as Buffer;
const id = this.get("id");
if (!id) throw new Error("id not found.");
return id.toString("utf8");
return bytesToUtf8(id);
}
get keypairType(): KeypairType {
@ -123,27 +127,31 @@ export class ENR extends Map<ENRKey, ENRValue> {
}
}
get publicKey(): Buffer {
get publicKey(): Uint8Array | undefined {
switch (this.id) {
case "v4":
return this.get("secp256k1") as Buffer;
return this.get("secp256k1");
default:
throw new Error(ERR_INVALID_ID);
}
}
get keypair(): IKeypair {
return createKeypair(this.keypairType, undefined, this.publicKey);
get keypair(): IKeypair | undefined {
if (this.publicKey) {
const publicKey = this.publicKey;
return createKeypair(this.keypairType, undefined, publicKey);
}
return;
}
get peerId(): PeerId {
return createPeerIdFromKeypair(this.keypair);
get peerId(): PeerId | undefined {
return this.keypair ? createPeerIdFromKeypair(this.keypair) : undefined;
}
get nodeId(): NodeId {
get nodeId(): NodeId | undefined {
switch (this.id) {
case "v4":
return v4.nodeId(this.publicKey);
return this.publicKey ? v4.nodeId(this.publicKey) : undefined;
default:
throw new Error(ERR_INVALID_ID);
}
@ -266,32 +274,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
get multiaddrs(): Multiaddr[] | undefined {
const raw = this.get("multiaddrs");
if (raw) {
const multiaddrs = [];
if (raw) return decodeMultiaddrs(raw);
try {
let index = 0;
while (index < raw.length) {
const sizeBytes = raw.slice(index, index + 2);
const size = Buffer.from(sizeBytes).readUInt16BE(0);
const multiaddrBytes = raw.slice(
index + MULTIADDR_LENGTH_SIZE,
index + size + MULTIADDR_LENGTH_SIZE
);
const multiaddr = new Multiaddr(multiaddrBytes);
multiaddrs.push(multiaddr);
index += size + MULTIADDR_LENGTH_SIZE;
}
} catch (e) {
throw new Error("Invalid value in multiaddrs field");
}
return multiaddrs;
} else {
return undefined;
}
return;
}
/**
@ -303,37 +288,14 @@ export class ENR extends Map<ENRKey, ENRValue> {
*
* If the peer information only contains information that can be represented with the ENR pre-defined keys
* (ip, tcp, etc) then the usage of [[setLocationMultiaddr]] should be preferred.
*
* The multiaddresses stored in this field must to be location multiaddresses, ie, peer id less.
* The multiaddresses stored in this field must be location multiaddresses,
* ie, without a peer id.
*/
set multiaddrs(multiaddrs: Multiaddr[] | undefined) {
if (multiaddrs === undefined) {
this.delete("multiaddrs");
} else {
let multiaddrsBuf = Buffer.from([]);
multiaddrs.forEach((multiaddr) => {
if (multiaddr.getPeerId())
throw new Error("`multiaddr` field MUST not contain peer id");
const bytes = multiaddr.bytes;
let buf = Buffer.alloc(2);
// Prepend the size of the next entry
const written = buf.writeUInt16BE(bytes.length, 0);
if (written !== MULTIADDR_LENGTH_SIZE) {
throw new Error(
`Internal error: unsigned 16-bit integer was not written in ${MULTIADDR_LENGTH_SIZE} bytes`
);
}
buf = Buffer.concat([buf, bytes]);
multiaddrsBuf = Buffer.concat([multiaddrsBuf, buf]);
});
const multiaddrsBuf = encodeMultiaddrs(multiaddrs);
this.set("multiaddrs", multiaddrsBuf);
}
}
@ -427,9 +389,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
getFullMultiaddr(
protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6"
): Multiaddr | undefined {
const locationMultiaddr = this.getLocationMultiaddr(protocol);
if (locationMultiaddr) {
return locationMultiaddr.encapsulate(`/p2p/${this.peerId.toB58String()}`);
if (this.peerId) {
const locationMultiaddr = this.getLocationMultiaddr(protocol);
if (locationMultiaddr) {
return locationMultiaddr.encapsulate(
`/p2p/${this.peerId.toB58String()}`
);
}
}
return;
}
@ -438,15 +404,16 @@ export class ENR extends Map<ENRKey, ENRValue> {
* Returns the full multiaddrs from the `multiaddrs` ENR field.
*/
getFullMultiaddrs(): Multiaddr[] {
if (this.multiaddrs) {
if (this.peerId && this.multiaddrs) {
const peerId = this.peerId;
return this.multiaddrs.map((ma) => {
return ma.encapsulate(`/p2p/${this.peerId.toB58String()}`);
return ma.encapsulate(`/p2p/${peerId.toB58String()}`);
});
}
return [];
}
verify(data: Buffer, signature: Buffer): boolean {
verify(data: Uint8Array, signature: Uint8Array): boolean {
if (!this.get("id") || this.id !== "v4") {
throw new Error(ERR_INVALID_ID);
}
@ -456,7 +423,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return v4.verify(this.publicKey, data, signature);
}
sign(data: Buffer, privateKey: Buffer): Buffer {
sign(data: Uint8Array, privateKey: Uint8Array): Uint8Array {
switch (this.id) {
case "v4":
this.signature = v4.sign(privateKey, data);
@ -467,7 +434,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return this.signature;
}
encodeToValues(privateKey?: Buffer): (ENRKey | ENRValue | number)[] {
encodeToValues(privateKey?: Uint8Array): (ENRKey | ENRValue | number)[] {
// sort keys and flatten into [k, v, k, v, ...]
const content: Array<ENRKey | ENRValue | number> = Array.from(this.keys())
.sort((a, b) => a.localeCompare(b))
@ -485,7 +452,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return content;
}
encode(privateKey?: Buffer): Buffer {
encode(privateKey?: Uint8Array): Uint8Array {
const encoded = RLP.encode(this.encodeToValues(privateKey));
if (encoded.length >= MAX_RECORD_SIZE) {
throw new Error("ENR must be less than 300 bytes");
@ -493,7 +460,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return encoded;
}
encodeTxt(privateKey?: Buffer): string {
return ENR.RECORD_PREFIX + base64url.encode(this.encode(privateKey));
async encodeTxt(privateKey?: Uint8Array): Promise<string> {
return ENR.RECORD_PREFIX + (await bytesToBase64(this.encode(privateKey)));
}
}

View File

@ -22,8 +22,8 @@ export async function generateKeypair(type: KeypairType): Promise<IKeypair> {
export function createKeypair(
type: KeypairType,
privateKey?: Buffer,
publicKey?: Buffer
privateKey?: Uint8Array,
publicKey?: Uint8Array
): IKeypair {
switch (type) {
case KeypairType.secp256k1:

View File

@ -1,25 +1,26 @@
import { Buffer } from "buffer";
import crypto from "crypto";
import * as secp256k1 from "secp256k1";
import { AbstractKeypair, IKeypair, IKeypairClass, KeypairType } from "./types";
export function secp256k1PublicKeyToCompressed(publicKey: Uint8Array): Buffer {
export function secp256k1PublicKeyToCompressed(
publicKey: Uint8Array
): Uint8Array {
if (publicKey.length === 64) {
publicKey = Buffer.concat([Buffer.from([4]), publicKey]);
}
return Buffer.from(secp256k1.publicKeyConvert(publicKey, true));
}
export function secp256k1PublicKeyToFull(publicKey: Uint8Array): Buffer {
export function secp256k1PublicKeyToFull(publicKey: Uint8Array): Uint8Array {
if (publicKey.length === 64) {
return Buffer.concat([Buffer.from([4]), publicKey]);
}
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false));
}
export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Buffer {
export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Uint8Array {
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false).slice(1));
}
@ -29,7 +30,7 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair
{
readonly type: KeypairType;
constructor(privateKey?: Buffer, publicKey?: Buffer) {
constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) {
let pub = publicKey;
if (pub) {
pub = secp256k1PublicKeyToCompressed(pub);
@ -58,12 +59,12 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair
return true;
}
sign(msg: Buffer): Buffer {
sign(msg: Uint8Array): Uint8Array {
const { signature, recid } = secp256k1.ecdsaSign(msg, this.privateKey);
return Buffer.concat([signature, Buffer.from([recid])]);
}
verify(msg: Buffer, sig: Buffer): boolean {
verify(msg: Uint8Array, sig: Uint8Array): boolean {
return secp256k1.ecdsaVerify(sig, msg, this.publicKey);
}
};

View File

@ -6,25 +6,25 @@ export enum KeypairType {
export interface IKeypair {
type: KeypairType;
privateKey: Buffer;
publicKey: Buffer;
privateKey: Uint8Array;
publicKey: Uint8Array;
privateKeyVerify(): boolean;
publicKeyVerify(): boolean;
sign(msg: Buffer): Buffer;
verify(msg: Buffer, sig: Buffer): boolean;
sign(msg: Uint8Array): Uint8Array;
verify(msg: Uint8Array, sig: Uint8Array): boolean;
hasPrivateKey(): boolean;
}
export interface IKeypairClass {
new (privateKey?: Buffer, publicKey?: Buffer): IKeypair;
new (privateKey?: Uint8Array, publicKey?: Uint8Array): IKeypair;
generate(): Promise<IKeypair>;
}
export abstract class AbstractKeypair {
_privateKey?: Buffer;
readonly _publicKey?: Buffer;
_privateKey?: Uint8Array;
readonly _publicKey?: Uint8Array;
constructor(privateKey?: Buffer, publicKey?: Buffer) {
constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) {
if ((this._privateKey = privateKey) && !this.privateKeyVerify()) {
throw new Error("Invalid private key");
}
@ -33,14 +33,14 @@ export abstract class AbstractKeypair {
}
}
get privateKey(): Buffer {
get privateKey(): Uint8Array {
if (!this._privateKey) {
throw new Error();
}
return this._privateKey;
}
get publicKey(): Buffer {
get publicKey(): Uint8Array {
if (!this._publicKey) {
throw new Error();
}

View File

@ -0,0 +1,34 @@
import { expect } from "chai";
import { Multiaddr } from "multiaddr";
import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec";
describe("ENR multiaddrs codec", function () {
it("Sample", async () => {
const multiaddrs = [
new Multiaddr(
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss"
),
new Multiaddr(
"/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss"
),
new Multiaddr(
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
),
];
const bytes = encodeMultiaddrs(multiaddrs);
const result = decodeMultiaddrs(bytes);
const multiaddrsAsStr = result.map((ma) => ma.toString());
expect(multiaddrsAsStr).to.include(
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss"
);
expect(multiaddrsAsStr).to.include(
"/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss"
);
expect(multiaddrsAsStr).to.include(
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
);
});
});

View File

@ -0,0 +1,50 @@
import { Multiaddr } from "multiaddr";
import { MULTIADDR_LENGTH_SIZE } from "./constants";
export function decodeMultiaddrs(bytes: Uint8Array): Multiaddr[] {
const multiaddrs = [];
let index = 0;
while (index < bytes.length) {
const sizeDataView = new DataView(
bytes.buffer,
index,
MULTIADDR_LENGTH_SIZE
);
const size = sizeDataView.getUint16(0);
index += MULTIADDR_LENGTH_SIZE;
const multiaddrBytes = bytes.slice(index, index + size);
index += size;
const multiaddr = new Multiaddr(multiaddrBytes);
multiaddrs.push(multiaddr);
}
return multiaddrs;
}
export function encodeMultiaddrs(multiaddrs: Multiaddr[]): Uint8Array {
const totalLength = multiaddrs.reduce(
(acc, ma) => acc + MULTIADDR_LENGTH_SIZE + ma.bytes.length,
0
);
const bytes = new Uint8Array(totalLength);
const dataView = new DataView(bytes.buffer);
let index = 0;
multiaddrs.forEach((multiaddr) => {
if (multiaddr.getPeerId())
throw new Error("`multiaddr` field MUST not contain peer id");
// Prepend the size of the next entry
dataView.setUint16(index, multiaddr.bytes.length);
index += MULTIADDR_LENGTH_SIZE;
bytes.set(multiaddr.bytes, index);
index += multiaddr.bytes.length;
});
return bytes;
}

View File

@ -6,24 +6,28 @@ import * as secp256k1 from "secp256k1";
import { createNodeId } from "./create";
import { NodeId } from "./types";
export function hash(input: Uint8Array): Buffer {
return Buffer.from(keccak256.arrayBuffer(input));
export function hash(input: Uint8Array): Uint8Array {
return new Uint8Array(keccak256.arrayBuffer(input));
}
export async function createPrivateKey(): Promise<Buffer> {
return Buffer.from(await randomBytes(32));
export async function createPrivateKey(): Promise<Uint8Array> {
return new Uint8Array(await randomBytes(32));
}
export function publicKey(privKey: Uint8Array): Buffer {
return Buffer.from(secp256k1.publicKeyCreate(privKey));
export function publicKey(privKey: Uint8Array): Uint8Array {
return new Uint8Array(secp256k1.publicKeyCreate(privKey));
}
export function sign(privKey: Uint8Array, msg: Uint8Array): Buffer {
export function sign(privKey: Uint8Array, msg: Uint8Array): Uint8Array {
const { signature } = secp256k1.ecdsaSign(hash(msg), privKey);
return Buffer.from(signature);
return new Uint8Array(signature);
}
export function verify(pubKey: Buffer, msg: Buffer, sig: Buffer): boolean {
export function verify(
pubKey: Uint8Array,
msg: Uint8Array,
sig: Uint8Array
): boolean {
// Remove the recovery id if present (byte #65)
return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey);
}
@ -37,11 +41,11 @@ export function nodeId(pubKey: Uint8Array): NodeId {
export class ENRKeyPair {
public constructor(
public readonly nodeId: NodeId,
public readonly privateKey: Buffer,
public readonly publicKey: Buffer
public readonly privateKey: Uint8Array,
public readonly publicKey: Uint8Array
) {}
public static async create(privateKey?: Buffer): Promise<ENRKeyPair> {
public static async create(privateKey?: Uint8Array): Promise<ENRKeyPair> {
if (privateKey) {
if (!secp256k1.privateKeyVerify(privateKey)) {
throw new Error("Invalid private key");
@ -54,11 +58,11 @@ export class ENRKeyPair {
return new ENRKeyPair(_nodeId, _privateKey, _publicKey);
}
public sign(msg: Buffer): Buffer {
public sign(msg: Uint8Array): Uint8Array {
return sign(this.privateKey, msg);
}
public verify(msg: Buffer, sig: Buffer): boolean {
public verify(msg: Uint8Array, sig: Uint8Array): boolean {
return verify(this.publicKey, msg, sig);
}
}

View File

@ -1,3 +1,6 @@
/**
* Decode bytes to utf-8 string.
*/
// Thanks https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
export function bytesToUtf8(bytes: Uint8Array): string {
let i = 0,
@ -41,3 +44,42 @@ export function bytesToUtf8(bytes: Uint8Array): string {
}
return s;
}
/**
* Encode utf-8 string to byte array
*/
// Thanks https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
export function utf8ToBytes(s: string): Uint8Array {
let i = 0;
const bytes = new Uint8Array(s.length * 4);
for (let ci = 0; ci != s.length; ci++) {
let c = s.charCodeAt(ci);
if (c < 128) {
bytes[i++] = c;
continue;
}
if (c < 2048) {
bytes[i++] = (c >> 6) | 192;
} else {
if (c > 0xd7ff && c < 0xdc00) {
if (++ci >= s.length)
throw new Error("UTF-8 encode: incomplete surrogate pair");
const c2 = s.charCodeAt(ci);
if (c2 < 0xdc00 || c2 > 0xdfff)
throw new Error(
"UTF-8 encode: second surrogate character 0x" +
c2.toString(16) +
" at index " +
ci +
" out of range"
);
c = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff);
bytes[i++] = (c >> 18) | 240;
bytes[i++] = ((c >> 12) & 63) | 128;
} else bytes[i++] = (c >> 12) | 224;
bytes[i++] = ((c >> 6) & 63) | 128;
}
bytes[i++] = (c & 63) | 128;
}
return bytes.subarray(0, i);
}

View File

@ -81,3 +81,49 @@ export function equalByteArrays(
export function keccak256Buf(message: Message): Buffer {
return Buffer.from(keccak256.arrayBuffer(message));
}
/**
* Convert base64 string to byte array.
*/
export function base64ToBytes(base64: string): Uint8Array {
const e = new Map<string, number>();
const len = base64.length;
const res = [];
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
for (let i = 0; i < 64; i++) {
e.set(A.charAt(i), i);
}
e.set("+", 62);
e.set("/", 63);
let b = 0,
l = 0,
a;
for (let i = 0; i < len; i++) {
const c = e.get(base64.charAt(i));
if (c === undefined)
throw new Error(`Invalid base64 character ${base64.charAt(i)}`);
b = (b << 6) + c;
l += 6;
while (l >= 8) {
((a = (b >>> (l -= 8)) & 0xff) || i < len - 2) && res.push(a);
}
}
return new Uint8Array(res);
}
/**
* Convert byte array to base64 string.
*/
export async function bytesToBase64(bytes: Uint8Array): Promise<string> {
const base64url: string = await new Promise((r) => {
const reader = new window.FileReader();
reader.onload = (): void => r(reader.result as string);
reader.readAsDataURL(new Blob([bytes]));
});
const base64 = base64url.split(",", 2)[1];
// We want URL and Filename Safe base64: https://datatracker.ietf.org/doc/html/rfc4648#section-5
// Without trailing padding
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

View File

@ -19,10 +19,7 @@ describe("Waku Dial", function () {
});
before(function () {
if (
process.env.CI ||
(typeof window !== "undefined" && window?.__env__?.CI)
) {
if (process.env.CI || window?.__env__?.CI) {
this.skip();
}
});

View File

@ -2,13 +2,13 @@ import { expect } from "chai";
import debug from "debug";
import {
bytesToUtf8,
makeLogFileName,
NimWaku,
NOISE_KEY_1,
WakuRelayMessage,
} from "../../test_utils";
import { delay } from "../../test_utils/delay";
import { bytesToUtf8 } from "../utf8";
import { hexToBytes } from "../utils";
import { Protocols, Waku } from "../waku";

View File

@ -9,4 +9,3 @@ export * from "./async_fs";
export * from "./constants";
export * from "./log_file";
export * from "./nim_waku";
export * from "./utf8";

View File

@ -335,7 +335,6 @@ export class NimWaku {
return { peerId: this.peerId, multiaddrWithId: this.multiaddrWithId };
}
const res = await this.info();
console.log(res);
this.multiaddrWithId = res.listenAddresses
.map((ma) => multiaddr(ma))
.find((ma) => ma.protoNames().includes("ws"));