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"], "extension": ["ts"],
"spec": "src/**/*.spec.ts", "spec": "src/**/*.spec.ts",
"require": ["ts-node/register", "isomorphic-fetch"], "require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
"exit": true "exit": true
} }

View File

@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- axios dependency in favour of fetch. - `axios` dependency in favour of fetch.
- `base64url` and `bigint-buffer` dependencies.
## [0.16.0] - 2022-01-31 ## [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:cname": "echo 'js-waku.wakuconnect.dev' > build/docs/CNAME",
"doc:examples": "mkdir -p build/docs/examples", "doc:examples": "mkdir -p build/docs/examples",
"deploy": "node ci/deploy.js", "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": { "engines": {
"node": ">=16" "node": ">=16"
}, },
"dependencies": { "dependencies": {
"@chainsafe/libp2p-noise": "^5.0.0", "@chainsafe/libp2p-noise": "^5.0.0",
"base64url": "^3.0.1",
"bigint-buffer": "^1.1.5",
"debug": "^4.3.1", "debug": "^4.3.1",
"dns-query": "^0.8.0", "dns-query": "^0.8.0",
"ecies-geth": "^1.5.2", "ecies-geth": "^1.5.2",
@ -107,6 +105,8 @@
"fast-check": "^2.14.0", "fast-check": "^2.14.0",
"gh-pages": "^3.2.3", "gh-pages": "^3.2.3",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12", "karma": "^6.3.12",
"karma-chrome-launcher": "^3.1.0", "karma-chrome-launcher": "^3.1.0",
"karma-env-preprocessor": "^0.1.1", "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 enr from "./lib/enr";
export * as utf8 from "./lib/utf8";
export * as utils from "./lib/utils"; export * as utils from "./lib/utils";
export * as waku from "./lib/waku"; export * as waku from "./lib/waku";

View File

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

View File

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

View File

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

View File

@ -2,9 +2,9 @@ import { bytesToHex } from "../utils";
import { NodeId } from "./types"; import { NodeId } from "./types";
export function createNodeId(buffer: Buffer): NodeId { export function createNodeId(bytes: Uint8Array): NodeId {
if (buffer.length !== 32) { if (bytes.length !== 32) {
throw new Error("NodeId must be 32 bytes in length"); 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" "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
), ),
]; ];
const txt = enr.encodeTxt(keypair.privateKey); const txt = await enr.encodeTxt(keypair.privateKey);
expect(txt.slice(0, 4)).to.be.equal("enr:");
const enr2 = ENR.decodeTxt(txt); const enr2 = ENR.decodeTxt(txt);
expect(bytesToHex(enr2.signature as Buffer)).to.be.equal( expect(bytesToHex(enr2.signature as Buffer)).to.be.equal(
bytesToHex(enr.signature as Buffer) bytesToHex(enr.signature as Buffer)
@ -87,17 +87,21 @@ describe("ENR", function () {
expect(enr.ip).to.not.be.undefined; expect(enr.ip).to.not.be.undefined;
expect(enr.ip).to.be.equal("134.209.139.210"); expect(enr.ip).to.be.equal("134.209.139.210");
expect(enr.publicKey).to.not.be.undefined; expect(enr.publicKey).to.not.be.undefined;
expect(enr.peerId.toB58String()).to.be.equal( expect(enr.peerId?.toB58String()).to.be.equal(
"16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ" "16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ"
); );
}); });
it("should throw error - no id", () => { it("should throw error - no id", async () => {
try { try {
const txt = Buffer.from( const peerId = await PeerId.create({ keyType: "secp256k1" });
"656e723a2d435972595a62404b574342526c4179357a7a61445a584a42476b636e68344d486342465a6e75584e467264764a6a5830346a527a6a7a", const enr = ENR.createFromPeerId(peerId);
"hex" const keypair = createKeypairFromPeerId(peerId);
).toString(); 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); ENR.decodeTxt(txt);
assert.fail("Expect error here"); assert.fail("Expect error here");
} catch (err: unknown) { } 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", () => { describe("Static tests", () => {
let privateKey: Buffer; let privateKey: Buffer;
let record: ENR; let record: ENR;
@ -212,9 +185,10 @@ describe("ENR", function () {
"hex" "hex"
); );
record = ENR.createV4(v4.publicKey(privateKey)); record = ENR.createV4(v4.publicKey(privateKey));
record.set("ip", Buffer.from("7f000001", "hex")); record.setLocationMultiaddr(new Multiaddr("/ip4/127.0.0.1/udp/30303"));
record.set("udp", Buffer.from((30303).toString(16), "hex"));
record.seq = seq; record.seq = seq;
// To set signature
record.encode(privateKey);
}); });
it("should properly compute the node id", () => { it("should properly compute the node id", () => {
@ -228,7 +202,7 @@ describe("ENR", function () {
expect(decoded).to.deep.equal(record); 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 // spec enr https://eips.ethereum.org/EIPS/eip-778
const testTxt = const testTxt =
"enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
@ -236,7 +210,8 @@ describe("ENR", function () {
expect(decoded.udp).to.be.equal(30303); expect(decoded.udp).to.be.equal(30303);
expect(decoded.ip).to.be.equal("127.0.0.1"); expect(decoded.ip).to.be.equal("127.0.0.1");
expect(decoded).to.deep.equal(record); 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"; import { Multiaddr, protocols } from "multiaddr";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available // @ts-ignore: No types available
@ -8,12 +6,10 @@ import PeerId from "peer-id";
import * as RLP from "rlp"; import * as RLP from "rlp";
import { encode as varintEncode } from "varint"; import { encode as varintEncode } from "varint";
import { import { bytesToUtf8, utf8ToBytes } from "../utf8";
ERR_INVALID_ID, import { base64ToBytes, bytesToBase64, bytesToHex } from "../utils";
ERR_NO_SIGNATURE,
MAX_RECORD_SIZE, import { ERR_INVALID_ID, ERR_NO_SIGNATURE, MAX_RECORD_SIZE } from "./constants";
MULTIADDR_LENGTH_SIZE,
} from "./constants";
import { import {
createKeypair, createKeypair,
createKeypairFromPeerId, createKeypairFromPeerId,
@ -21,28 +17,32 @@ import {
IKeypair, IKeypair,
KeypairType, KeypairType,
} from "./keypair"; } from "./keypair";
import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec";
import { ENRKey, ENRValue, NodeId, SequenceNumber } from "./types"; import { ENRKey, ENRValue, NodeId, SequenceNumber } from "./types";
import * as v4 from "./v4"; import * as v4 from "./v4";
export class ENR extends Map<ENRKey, ENRValue> { export class ENR extends Map<ENRKey, ENRValue> {
public static readonly RECORD_PREFIX = "enr:"; public static readonly RECORD_PREFIX = "enr:";
public seq: SequenceNumber; public seq: SequenceNumber;
public signature: Buffer | null; public signature: Uint8Array | null;
constructor( constructor(
kvs: Record<ENRKey, ENRValue> = {}, kvs: Record<ENRKey, ENRValue> = {},
seq: SequenceNumber = 1n, seq: SequenceNumber = 1n,
signature: Buffer | null = null signature: Uint8Array | null = null
) { ) {
super(Object.entries(kvs)); super(Object.entries(kvs));
this.seq = seq; this.seq = seq;
this.signature = signature; this.signature = signature;
} }
static createV4(publicKey: Buffer, kvs: Record<ENRKey, ENRValue> = {}): ENR { static createV4(
publicKey: Uint8Array,
kvs: Record<ENRKey, ENRValue> = {}
): ENR {
return new ENR({ return new ENR({
...kvs, ...kvs,
id: Buffer.from("v4"), id: utf8ToBytes("v4"),
secp256k1: publicKey, secp256k1: publicKey,
}); });
} }
@ -78,9 +78,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
} }
const obj: Record<ENRKey, ENRValue> = {}; const obj: Record<ENRKey, ENRValue> = {};
for (let i = 0; i < kvs.length; i += 2) { 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)) { if (!enr.verify(RLP.encode([seq, ...kvs]), signature)) {
throw new Error("Unable to verify ENR signature"); throw new Error("Unable to verify ENR signature");
@ -88,7 +92,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return enr; return enr;
} }
static decode(encoded: Buffer): ENR { static decode(encoded: Uint8Array): ENR {
const decoded = RLP.decode(encoded) as unknown as Buffer[]; const decoded = RLP.decode(encoded) as unknown as Buffer[];
return ENR.decodeFromValues(decoded); return ENR.decodeFromValues(decoded);
} }
@ -99,7 +103,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
`"string encoded ENR must start with '${this.RECORD_PREFIX}'` `"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 { set(k: ENRKey, v: ENRValue): this {
@ -109,9 +113,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
} }
get id(): string { get id(): string {
const id = this.get("id") as Buffer; const id = this.get("id");
if (!id) throw new Error("id not found."); if (!id) throw new Error("id not found.");
return id.toString("utf8"); return bytesToUtf8(id);
} }
get keypairType(): KeypairType { get keypairType(): KeypairType {
@ -123,27 +127,31 @@ export class ENR extends Map<ENRKey, ENRValue> {
} }
} }
get publicKey(): Buffer { get publicKey(): Uint8Array | undefined {
switch (this.id) { switch (this.id) {
case "v4": case "v4":
return this.get("secp256k1") as Buffer; return this.get("secp256k1");
default: default:
throw new Error(ERR_INVALID_ID); throw new Error(ERR_INVALID_ID);
} }
} }
get keypair(): IKeypair { get keypair(): IKeypair | undefined {
return createKeypair(this.keypairType, undefined, this.publicKey); if (this.publicKey) {
const publicKey = this.publicKey;
return createKeypair(this.keypairType, undefined, publicKey);
}
return;
} }
get peerId(): PeerId { get peerId(): PeerId | undefined {
return createPeerIdFromKeypair(this.keypair); return this.keypair ? createPeerIdFromKeypair(this.keypair) : undefined;
} }
get nodeId(): NodeId { get nodeId(): NodeId | undefined {
switch (this.id) { switch (this.id) {
case "v4": case "v4":
return v4.nodeId(this.publicKey); return this.publicKey ? v4.nodeId(this.publicKey) : undefined;
default: default:
throw new Error(ERR_INVALID_ID); throw new Error(ERR_INVALID_ID);
} }
@ -266,32 +274,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
get multiaddrs(): Multiaddr[] | undefined { get multiaddrs(): Multiaddr[] | undefined {
const raw = this.get("multiaddrs"); const raw = this.get("multiaddrs");
if (raw) { if (raw) return decodeMultiaddrs(raw);
const multiaddrs = [];
try { return;
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;
}
} }
/** /**
@ -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 * 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. * (ip, tcp, etc) then the usage of [[setLocationMultiaddr]] should be preferred.
* * The multiaddresses stored in this field must be location multiaddresses,
* The multiaddresses stored in this field must to be location multiaddresses, ie, peer id less. * ie, without a peer id.
*/ */
set multiaddrs(multiaddrs: Multiaddr[] | undefined) { set multiaddrs(multiaddrs: Multiaddr[] | undefined) {
if (multiaddrs === undefined) { if (multiaddrs === undefined) {
this.delete("multiaddrs"); this.delete("multiaddrs");
} else { } else {
let multiaddrsBuf = Buffer.from([]); const multiaddrsBuf = encodeMultiaddrs(multiaddrs);
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]);
});
this.set("multiaddrs", multiaddrsBuf); this.set("multiaddrs", multiaddrsBuf);
} }
} }
@ -427,9 +389,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
getFullMultiaddr( getFullMultiaddr(
protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6" protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6"
): Multiaddr | undefined { ): Multiaddr | undefined {
if (this.peerId) {
const locationMultiaddr = this.getLocationMultiaddr(protocol); const locationMultiaddr = this.getLocationMultiaddr(protocol);
if (locationMultiaddr) { if (locationMultiaddr) {
return locationMultiaddr.encapsulate(`/p2p/${this.peerId.toB58String()}`); return locationMultiaddr.encapsulate(
`/p2p/${this.peerId.toB58String()}`
);
}
} }
return; return;
} }
@ -438,15 +404,16 @@ export class ENR extends Map<ENRKey, ENRValue> {
* Returns the full multiaddrs from the `multiaddrs` ENR field. * Returns the full multiaddrs from the `multiaddrs` ENR field.
*/ */
getFullMultiaddrs(): Multiaddr[] { getFullMultiaddrs(): Multiaddr[] {
if (this.multiaddrs) { if (this.peerId && this.multiaddrs) {
const peerId = this.peerId;
return this.multiaddrs.map((ma) => { return this.multiaddrs.map((ma) => {
return ma.encapsulate(`/p2p/${this.peerId.toB58String()}`); return ma.encapsulate(`/p2p/${peerId.toB58String()}`);
}); });
} }
return []; return [];
} }
verify(data: Buffer, signature: Buffer): boolean { verify(data: Uint8Array, signature: Uint8Array): boolean {
if (!this.get("id") || this.id !== "v4") { if (!this.get("id") || this.id !== "v4") {
throw new Error(ERR_INVALID_ID); throw new Error(ERR_INVALID_ID);
} }
@ -456,7 +423,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return v4.verify(this.publicKey, data, signature); return v4.verify(this.publicKey, data, signature);
} }
sign(data: Buffer, privateKey: Buffer): Buffer { sign(data: Uint8Array, privateKey: Uint8Array): Uint8Array {
switch (this.id) { switch (this.id) {
case "v4": case "v4":
this.signature = v4.sign(privateKey, data); this.signature = v4.sign(privateKey, data);
@ -467,7 +434,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return this.signature; return this.signature;
} }
encodeToValues(privateKey?: Buffer): (ENRKey | ENRValue | number)[] { encodeToValues(privateKey?: Uint8Array): (ENRKey | ENRValue | number)[] {
// sort keys and flatten into [k, v, k, v, ...] // sort keys and flatten into [k, v, k, v, ...]
const content: Array<ENRKey | ENRValue | number> = Array.from(this.keys()) const content: Array<ENRKey | ENRValue | number> = Array.from(this.keys())
.sort((a, b) => a.localeCompare(b)) .sort((a, b) => a.localeCompare(b))
@ -485,7 +452,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return content; return content;
} }
encode(privateKey?: Buffer): Buffer { encode(privateKey?: Uint8Array): Uint8Array {
const encoded = RLP.encode(this.encodeToValues(privateKey)); const encoded = RLP.encode(this.encodeToValues(privateKey));
if (encoded.length >= MAX_RECORD_SIZE) { if (encoded.length >= MAX_RECORD_SIZE) {
throw new Error("ENR must be less than 300 bytes"); throw new Error("ENR must be less than 300 bytes");
@ -493,7 +460,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return encoded; return encoded;
} }
encodeTxt(privateKey?: Buffer): string { async encodeTxt(privateKey?: Uint8Array): Promise<string> {
return ENR.RECORD_PREFIX + base64url.encode(this.encode(privateKey)); 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( export function createKeypair(
type: KeypairType, type: KeypairType,
privateKey?: Buffer, privateKey?: Uint8Array,
publicKey?: Buffer publicKey?: Uint8Array
): IKeypair { ): IKeypair {
switch (type) { switch (type) {
case KeypairType.secp256k1: case KeypairType.secp256k1:

View File

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

View File

@ -6,25 +6,25 @@ export enum KeypairType {
export interface IKeypair { export interface IKeypair {
type: KeypairType; type: KeypairType;
privateKey: Buffer; privateKey: Uint8Array;
publicKey: Buffer; publicKey: Uint8Array;
privateKeyVerify(): boolean; privateKeyVerify(): boolean;
publicKeyVerify(): boolean; publicKeyVerify(): boolean;
sign(msg: Buffer): Buffer; sign(msg: Uint8Array): Uint8Array;
verify(msg: Buffer, sig: Buffer): boolean; verify(msg: Uint8Array, sig: Uint8Array): boolean;
hasPrivateKey(): boolean; hasPrivateKey(): boolean;
} }
export interface IKeypairClass { export interface IKeypairClass {
new (privateKey?: Buffer, publicKey?: Buffer): IKeypair; new (privateKey?: Uint8Array, publicKey?: Uint8Array): IKeypair;
generate(): Promise<IKeypair>; generate(): Promise<IKeypair>;
} }
export abstract class AbstractKeypair { export abstract class AbstractKeypair {
_privateKey?: Buffer; _privateKey?: Uint8Array;
readonly _publicKey?: Buffer; readonly _publicKey?: Uint8Array;
constructor(privateKey?: Buffer, publicKey?: Buffer) { constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) {
if ((this._privateKey = privateKey) && !this.privateKeyVerify()) { if ((this._privateKey = privateKey) && !this.privateKeyVerify()) {
throw new Error("Invalid private key"); throw new Error("Invalid private key");
} }
@ -33,14 +33,14 @@ export abstract class AbstractKeypair {
} }
} }
get privateKey(): Buffer { get privateKey(): Uint8Array {
if (!this._privateKey) { if (!this._privateKey) {
throw new Error(); throw new Error();
} }
return this._privateKey; return this._privateKey;
} }
get publicKey(): Buffer { get publicKey(): Uint8Array {
if (!this._publicKey) { if (!this._publicKey) {
throw new Error(); 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 { createNodeId } from "./create";
import { NodeId } from "./types"; import { NodeId } from "./types";
export function hash(input: Uint8Array): Buffer { export function hash(input: Uint8Array): Uint8Array {
return Buffer.from(keccak256.arrayBuffer(input)); return new Uint8Array(keccak256.arrayBuffer(input));
} }
export async function createPrivateKey(): Promise<Buffer> { export async function createPrivateKey(): Promise<Uint8Array> {
return Buffer.from(await randomBytes(32)); return new Uint8Array(await randomBytes(32));
} }
export function publicKey(privKey: Uint8Array): Buffer { export function publicKey(privKey: Uint8Array): Uint8Array {
return Buffer.from(secp256k1.publicKeyCreate(privKey)); 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); 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) // Remove the recovery id if present (byte #65)
return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey); return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey);
} }
@ -37,11 +41,11 @@ export function nodeId(pubKey: Uint8Array): NodeId {
export class ENRKeyPair { export class ENRKeyPair {
public constructor( public constructor(
public readonly nodeId: NodeId, public readonly nodeId: NodeId,
public readonly privateKey: Buffer, public readonly privateKey: Uint8Array,
public readonly publicKey: Buffer public readonly publicKey: Uint8Array
) {} ) {}
public static async create(privateKey?: Buffer): Promise<ENRKeyPair> { public static async create(privateKey?: Uint8Array): Promise<ENRKeyPair> {
if (privateKey) { if (privateKey) {
if (!secp256k1.privateKeyVerify(privateKey)) { if (!secp256k1.privateKeyVerify(privateKey)) {
throw new Error("Invalid private key"); throw new Error("Invalid private key");
@ -54,11 +58,11 @@ export class ENRKeyPair {
return new ENRKeyPair(_nodeId, _privateKey, _publicKey); return new ENRKeyPair(_nodeId, _privateKey, _publicKey);
} }
public sign(msg: Buffer): Buffer { public sign(msg: Uint8Array): Uint8Array {
return sign(this.privateKey, msg); 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); 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 // Thanks https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
export function bytesToUtf8(bytes: Uint8Array): string { export function bytesToUtf8(bytes: Uint8Array): string {
let i = 0, let i = 0,
@ -41,3 +44,42 @@ export function bytesToUtf8(bytes: Uint8Array): string {
} }
return s; 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 { export function keccak256Buf(message: Message): Buffer {
return Buffer.from(keccak256.arrayBuffer(message)); 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 () { before(function () {
if ( if (process.env.CI || window?.__env__?.CI) {
process.env.CI ||
(typeof window !== "undefined" && window?.__env__?.CI)
) {
this.skip(); this.skip();
} }
}); });

View File

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

View File

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

View File

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