Skip to content

Commit

Permalink
feat!: add pluggable enr crypto interface (#266)
Browse files Browse the repository at this point in the history
  • Loading branch information
wemeetagain committed Nov 16, 2023
1 parent 04152c1 commit 3475758
Show file tree
Hide file tree
Showing 24 changed files with 253 additions and 236 deletions.
8 changes: 4 additions & 4 deletions bench/enr/index.bench.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {itBench, setBenchOpts} from "@dapplion/benchmark";
import {generateKeypair, KeypairType} from "../../src/keypair";
import {ENR} from "../../src/enr";
import {generateKeypair} from "../../src/keypair";
import {SignableENR} from "../../src/enr";

describe("ENR", function() {
setBenchOpts({runs: 50000});

const keypairWithPrivateKey = generateKeypair(KeypairType.secp256k1);
const enr = ENR.createV4(keypairWithPrivateKey.privateKey);
const keypairWithPrivateKey = generateKeypair("secp256k1");
const enr = SignableENR.createV4(keypairWithPrivateKey);
enr.ip = "127.0.0.1";
enr.tcp = 8080;

Expand Down
17 changes: 8 additions & 9 deletions bench/keypair/index.bench.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import {itBench, setBenchOpts} from "@dapplion/benchmark";
import {createPeerIdFromKeypair, generateKeypair, KeypairType} from "../../src/keypair";
import {generateKeypair} from "../../src/keypair/index.js";
import { createPeerIdFromPrivateKey, createPeerIdFromPublicKey } from "../../src/enr/index.js";

describe("createPeerIdFromKeypair", function() {
describe("createPeerIdFromPrivateKey", function() {
setBenchOpts({runs: 4000});

const keypairWithPrivateKey = generateKeypair(KeypairType.secp256k1);
const keypairWithoutPrivateKey = generateKeypair(KeypairType.secp256k1);
delete (keypairWithoutPrivateKey as any)._privateKey;
const keypair = generateKeypair("secp256k1");

itBench("createPeerIdFromKeypair - private key", () => {
return createPeerIdFromKeypair(keypairWithPrivateKey);
itBench("createPeerIdFromPrivateKey", () => {
return createPeerIdFromPrivateKey(keypair.type, keypair.privateKey);
});
itBench("createPeerIdFromKeypair - no private key", () => {
return createPeerIdFromKeypair(keypairWithoutPrivateKey);
itBench("createPeerIdFromPublicKey", () => {
return createPeerIdFromPublicKey(keypair.type, keypair.publicKey);
});
});
117 changes: 62 additions & 55 deletions src/enr/enr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,41 @@ import { Multiaddr, multiaddr, protocols } from "@multiformats/multiaddr";
import base64url from "base64url";
import { toBigIntBE } from "bigint-buffer";
import * as RLP from "rlp";
import { KeyType } from "@libp2p/interface/keys";
import { PeerId } from "@libp2p/interface/peer-id";
import { convertToString, convertToBytes } from "@multiformats/multiaddr/convert";
import { encode as varintEncode } from "uint8-varint";

import { ERR_INVALID_ID, MAX_RECORD_SIZE } from "./constants.js";
import * as v4 from "./v4.js";
import * as bcryptoV4Crypto from "./v4.js";
import { ENRKey, ENRValue, SequenceNumber, NodeId } from "./types.js";
import {
createKeypair,
KeypairType,
IKeypair,
createPeerIdFromKeypair,
createKeypairFromPeerId,
} from "../keypair/index.js";
import { toNewUint8Array } from "../util/index.js";
import { createPeerIdFromPublicKey, createPrivateKeyFromPeerId } from "./peerId.js";
import { toNewUint8Array } from "./util.js";

/** ENR identity scheme */
export enum IDScheme {
v4 = "v4",
}

// In order to support different environments (eg: browser vs high performance), a pluggable crypto interface is provided

export type V4Crypto = {
publicKey(privKey: Uint8Array): Uint8Array;
sign(privKey: Uint8Array, msg: Uint8Array): Uint8Array;
verify(pubKey: Uint8Array, msg: Uint8Array, sig: Uint8Array): boolean;
nodeId(pubKey: Uint8Array): NodeId;
};

let v4: V4Crypto = bcryptoV4Crypto;

export function setV4Crypto(crypto: V4Crypto): void {
v4 = crypto;
}

export function getV4Crypto(): V4Crypto {
return v4;
}

/** Raw data included in an ENR */
export type ENRData = {
kvs: ReadonlyMap<ENRKey, ENRValue>;
Expand All @@ -47,7 +61,7 @@ export function id(kvs: ReadonlyMap<ENRKey, ENRValue>): IDScheme {
return id;
}

export function nodeId(id: IDScheme, publicKey: Buffer): NodeId {
export function nodeId(id: IDScheme, publicKey: Uint8Array): NodeId {
switch (id) {
case IDScheme.v4:
return v4.nodeId(publicKey);
Expand All @@ -68,27 +82,27 @@ export function publicKey(id: IDScheme, kvs: ReadonlyMap<ENRKey, ENRValue>): Uin
throw new Error(ERR_INVALID_ID);
}
}
export function keypairType(id: IDScheme): KeypairType {
export function keyType(id: IDScheme): KeyType {
switch (id) {
case "v4":
return KeypairType.Secp256k1;
return "secp256k1";
default:
throw new Error(ERR_INVALID_ID);
}
}

export function verify(id: IDScheme, data: Uint8Array, publicKey: Buffer, signature: Uint8Array): boolean {
export function verify(id: IDScheme, data: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): boolean {
switch (id) {
case IDScheme.v4:
return v4.verify(publicKey, Buffer.from(data), Buffer.from(signature));
return v4.verify(publicKey, data, signature);
default:
throw new Error(ERR_INVALID_ID);
}
}
export function sign(id: IDScheme, data: Uint8Array, privateKey: Buffer): Buffer {
export function sign(id: IDScheme, data: Uint8Array, privateKey: Uint8Array): Uint8Array {
switch (id) {
case IDScheme.v4:
return v4.sign(privateKey, Buffer.from(data));
return v4.sign(privateKey, data);
default:
throw new Error(ERR_INVALID_ID);
}
Expand Down Expand Up @@ -144,7 +158,7 @@ export function decodeFromValues(decoded: Uint8Array[]): ENRData {
signed.push(k, v);
}
const _id = id(kvs);
if (!verify(_id, RLP.encode(signed), Buffer.from(publicKey(_id, kvs)), signature)) {
if (!verify(_id, RLP.encode(signed), publicKey(_id, kvs), signature)) {
throw new Error("Unable to verify enr signature");
}
return {
Expand Down Expand Up @@ -210,17 +224,16 @@ export abstract class BaseENR {
/** Node identifier */
public abstract nodeId: NodeId;
public abstract publicKey: Uint8Array;
public abstract keypair: IKeypair;

/** enr identity scheme */
get id(): IDScheme {
return id(this.kvs);
}
get keypairType(): KeypairType {
return keypairType(this.id);
get keypairType(): KeyType {
return keyType(this.id);
}
async peerId(): Promise<PeerId> {
return createPeerIdFromKeypair(this.keypair);
return createPeerIdFromPublicKey(this.keypairType, this.publicKey);
}

// Network methods
Expand Down Expand Up @@ -330,7 +343,7 @@ export class ENR extends BaseENR {
this.kvs = new Map(kvs instanceof Map ? kvs.entries() : Object.entries(kvs));
this.seq = seq;
this.signature = signature;
this.nodeId = nodeId(this.id, Buffer.from(this.publicKey));
this.nodeId = nodeId(this.id, this.publicKey);
this.encoded = encoded;
}

Expand All @@ -351,9 +364,6 @@ export class ENR extends BaseENR {
return new ENR(kvs, seq, signature, encodedBuf);
}

get keypair(): IKeypair {
return createKeypair(this.keypairType, undefined, Buffer.from(this.publicKey));
}
get publicKey(): Uint8Array {
return publicKey(this.id, this.kvs);
}
Expand Down Expand Up @@ -387,72 +397,72 @@ export class ENR extends BaseENR {
export class SignableENR extends BaseENR {
public kvs: ReadonlyMap<ENRKey, ENRValue>;
public seq: SequenceNumber;
public keypair: IKeypair;
public nodeId: NodeId;
public publicKey: Uint8Array;
public privateKey: Uint8Array;
private _signature?: Uint8Array;

constructor(
kvs: ReadonlyMap<ENRKey, ENRValue> | Record<ENRKey, ENRValue> = {},
seq: SequenceNumber = 1n,
keypair: IKeypair,
privateKey: Uint8Array,
signature?: Uint8Array
) {
super();
this.kvs = new Map(kvs instanceof Map ? kvs.entries() : Object.entries(kvs));
this.seq = seq;
this.keypair = keypair;
this.nodeId = nodeId(this.id, Buffer.from(this.publicKey));
this.privateKey = privateKey;
this.publicKey = publicKey(this.id, this.kvs);
this.nodeId = nodeId(this.id, this.publicKey);
this._signature = signature;

if (!this.keypair.publicKey.equals(publicKey(this.id, this.kvs))) {
throw new Error("Provided keypair doesn't match kv pubkey");
if (this.id === IDScheme.v4) {
if (Buffer.compare(v4.publicKey(this.privateKey), this.publicKey) !== 0) {
throw new Error("Provided keypair doesn't match kv pubkey");
}
}
}

static fromObject(obj: SignableENRData): SignableENR {
const _id = id(obj.kvs);
return new SignableENR(
obj.kvs,
obj.seq,
createKeypair(keypairType(_id), Buffer.from(obj.privateKey), Buffer.from(publicKey(_id, obj.kvs)))
);
return new SignableENR(obj.kvs, obj.seq, obj.privateKey);
}
static createV4(keypair: IKeypair, kvs: Record<ENRKey, ENRValue> = {}): SignableENR {
static createV4(privateKey: Uint8Array, kvs: Record<ENRKey, ENRValue> = {}): SignableENR {
return new SignableENR(
{
...kvs,
id: Buffer.from("v4"),
secp256k1: keypair.publicKey,
secp256k1: v4.publicKey(privateKey),
},
BigInt(1),
keypair
privateKey
);
}
static createFromPeerId(peerId: PeerId, kvs: Record<ENRKey, ENRValue> = {}): SignableENR {
const keypair = createKeypairFromPeerId(peerId);
switch (keypair.type) {
case KeypairType.Secp256k1:
return SignableENR.createV4(keypair, kvs);
const { type, privateKey } = createPrivateKeyFromPeerId(peerId);
switch (type) {
case "secp256k1":
return SignableENR.createV4(privateKey, kvs);
default:
throw new Error();
}
}
static decodeFromValues(encoded: Uint8Array[], keypair: IKeypair): SignableENR {
static decodeFromValues(encoded: Uint8Array[], privateKey: Uint8Array): SignableENR {
const { kvs, seq, signature } = decodeFromValues(encoded);
return new SignableENR(kvs, seq, keypair, signature);
return new SignableENR(kvs, seq, privateKey, signature);
}
static decode(encoded: Uint8Array, keypair: IKeypair): SignableENR {
static decode(encoded: Uint8Array, privateKey: Uint8Array): SignableENR {
const { kvs, seq, signature } = decode(encoded);
return new SignableENR(kvs, seq, keypair, signature);
return new SignableENR(kvs, seq, privateKey, signature);
}
static decodeTxt(encoded: string, keypair: IKeypair): SignableENR {
static decodeTxt(encoded: string, privateKey: Uint8Array): SignableENR {
const { kvs, seq, signature } = decodeTxt(encoded);
return new SignableENR(kvs, seq, keypair, signature);
return new SignableENR(kvs, seq, privateKey, signature);
}

get signature(): Uint8Array {
if (!this._signature) {
this._signature = sign(this.id, RLP.encode(encodeToValues(this.kvs, this.seq)), this.keypair.privateKey);
this._signature = sign(this.id, RLP.encode(encodeToValues(this.kvs, this.seq)), this.privateKey);
}
return this._signature;
}
Expand All @@ -477,11 +487,8 @@ export class SignableENR extends BaseENR {

// Identity methods

get publicKey(): Buffer {
return this.keypair.publicKey;
}
async peerId(): Promise<PeerId> {
return createPeerIdFromKeypair(this.keypair);
return createPeerIdFromPublicKey(this.keypairType, this.publicKey);
}

// Network methods
Expand Down Expand Up @@ -570,7 +577,7 @@ export class SignableENR extends BaseENR {
return {
kvs: this.kvs,
seq: this.seq,
privateKey: new Uint8Array(this.keypair.privateKey),
privateKey: this.privateKey,
};
}
encodeToValues(): (string | number | Uint8Array)[] {
Expand Down
3 changes: 1 addition & 2 deletions src/enr/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as v4Crypto from "./v4.js";
export const v4 = v4Crypto;
export * from "./constants.js";
export * from "./enr.js";
export * from "./types.js";
export * from "./create.js";
export * from "./peerId.js";
52 changes: 52 additions & 0 deletions src/enr/peerId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PeerId } from "@libp2p/interface/peer-id";
import { peerIdFromKeys } from "@libp2p/peer-id";
import { keysPBM, supportedKeys } from "@libp2p/crypto/keys";
import { KeyType } from "@libp2p/interface/keys";
import { ERR_TYPE_NOT_IMPLEMENTED } from "../keypair/constants.js";

/** Translate to KeyType from keysPBM enum */
enum KeyTypeTranslator {
RSA = "RSA",
Ed25519 = "Ed25519",
Secp256k1 = "secp256k1",
}

export async function createPeerIdFromPublicKey(type: KeyType, publicKey: Uint8Array): Promise<PeerId> {
switch (type) {
case "secp256k1": {
const pubKey = new supportedKeys.secp256k1.Secp256k1PublicKey(publicKey);
return peerIdFromKeys(pubKey.bytes);
}
default:
throw new Error(ERR_TYPE_NOT_IMPLEMENTED);
}
}

export async function createPeerIdFromPrivateKey(type: KeyType, privateKey: Uint8Array): Promise<PeerId> {
switch (type) {
case "secp256k1": {
const privKey = new supportedKeys.secp256k1.Secp256k1PrivateKey(privateKey);
return peerIdFromKeys(privKey.public.bytes, privKey.bytes);
}
default:
throw new Error(ERR_TYPE_NOT_IMPLEMENTED);
}
}

export function createPublicKeyFromPeerId(peerId: PeerId): { type: KeyType; publicKey: Uint8Array } {
// pub/privkey bytes from peer-id are encoded in protobuf format
if (!peerId.publicKey) {
throw new Error("Public key required");
}
const pub = keysPBM.PublicKey.decode(peerId.publicKey);
return { type: KeyTypeTranslator[pub.Type!], publicKey: pub.Data! };
}

export function createPrivateKeyFromPeerId(peerId: PeerId): { type: KeyType; privateKey: Uint8Array } {
// pub/privkey bytes from peer-id are encoded in protobuf format
if (!peerId.privateKey) {
throw new Error("Private key required");
}
const priv = keysPBM.PrivateKey.decode(peerId.privateKey);
return { type: KeyTypeTranslator[priv.Type!], privateKey: priv.Data! };
}
5 changes: 5 additions & 0 deletions src/enr/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// multiaddr 8.0.0 expects an Uint8Array with internal buffer starting at 0 offset
export function toNewUint8Array(buf: Uint8Array): Uint8Array {
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
return new Uint8Array(arrayBuffer);
}
Loading

0 comments on commit 3475758

Please sign in to comment.