diff --git a/package.json b/package.json index 50574c4..f930e41 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,16 @@ "homepage": "https://github.com/libp2p/js-peer-id", "devDependencies": { "aegir": "^36.0.1", - "util": "^0.12.3" + "util": "^0.12.3", + "libp2p-interfaces-compliance-tests": "^2.0.6" }, "dependencies": { "class-is": "^1.1.0", "libp2p-crypto": "^0.21.0", "multiformats": "^9.4.5", "protobufjs": "^6.10.2", - "uint8arrays": "^3.0.0" + "uint8arrays": "^3.0.0", + "libp2p-interfaces": "^2.0.5" }, "repository": { "type": "git", diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 7ca21fc..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { PrivateKey, PublicKey, KeyType } from 'libp2p-crypto' -import { CID } from 'multiformats/cid' - -declare namespace PeerId { - /** - * Options for PeerId creation. - */ - interface CreateOptions { - /** - * The number of bits to use. - */ - bits?: number - /** - * The type of key to use. - */ - keyType?: KeyType - } - - /** - * PeerId JSON format. - */ - interface JSONPeerId { - /** - * String representation of PeerId. - */ - id: string - /** - * Public key. - */ - pubKey?: string - /** - * Private key. - */ - privKey?: string - } - - /** - * Checks if a value is an instance of PeerId. - * - * @param id - The value to check. - */ - function isPeerId (id: any): id is PeerId - - /** - * Create a new PeerId. - * - * @param opts - Options. - */ - function create (opts?: PeerId.CreateOptions): Promise - - /** - * Create PeerId from hex string. - * - * @param str - The input hex string. - */ - function createFromHexString (str: string): PeerId - - /** - * Create PeerId from raw bytes. - * - * @param buf - The raw bytes. - */ - function createFromBytes (buf: Uint8Array): PeerId - - /** - * Create PeerId from base58-encoded string. - * - * @param str - The base58-encoded string. - */ - function createFromB58String (str: string): PeerId - - /** - * Create PeerId from CID. - * - * @param cid - The CID. - */ - function createFromCID (cid: CID): PeerId - - /** - * Create PeerId from public key. - * - * @param key - Public key, as Uint8Array or base64-encoded string. - */ - function createFromPubKey (key: Uint8Array | string): Promise - - /** - * Create PeerId from private key. - * - * @param key - Private key, as Uint8Array or base64-encoded string. - */ - function createFromPrivKey (key: Uint8Array | string): Promise - - /** - * Create PeerId from PeerId JSON formatted object. - * - * @see {@link PeerId#toJSON} - * @param json - PeerId in JSON format. - */ - function createFromJSON (json: JSONPeerId): Promise - - /** - * Create PeerId from Protobuf bytes. - * - * @param buf - Protobuf bytes, as Uint8Array or hex-encoded string. - */ - function createFromProtobuf (buf: Uint8Array | string): Promise - - /** - * Parse a PeerId from a string. - * - * @param str - encoded public key string. - */ - function parse (str: string): PeerId -} - -/** - * PeerId is an object representation of a peer identifier. - */ -declare class PeerId { - constructor (id: Uint8Array, privKey?: PrivateKey, pubKey?: PublicKey); - - /** - * Raw id. - */ - readonly id: Uint8Array - - /** - * Private key. - */ - privKey: PrivateKey - - /** - * Public key. - */ - pubKey: PublicKey - - /** - * Return the protobuf version of the public key, matching go ipfs formatting. - */ - marshalPubKey (): Uint8Array; - - /** - * Return the protobuf version of the private key, matching go ipfs formatting. - */ - marshalPrivKey (): Uint8Array; - - /** - * Return the protobuf version of the peer-id. - * - * @param excludePriv - Whether to exclude the private key information from the output. - */ - marshal (excludePriv?: boolean): Uint8Array; - - /** - * String representation. - */ - toPrint (): string; - - /** - * Return the jsonified version of the key. - * Matches the formatting of go-ipfs for its config file. - * - * @see {@link PeerId.createFromJSON} - */ - toJSON (): PeerId.JSONPeerId; - - /** - * Encode to hex. - */ - toHexString (): string; - - /** - * Return raw id bytes. - */ - toBytes (): Uint8Array; - - /** - * Encode to base58 string. - */ - toB58String (): string; - - /** - * Return self-describing string representation. - * Uses default format from RFC 0001: https://github.com/libp2p/specs/pull/209 - */ - toString (): string; - - /** - * Checks the equality of `this` peer against a given PeerId. - * - * @param id - The other PeerId. - */ - equals (id: PeerId | Uint8Array): boolean; - - /** - * Checks the equality of `this` peer against a given PeerId. - * - * @deprecated Use {.equals} - * @param id - The other PeerId. - */ - isEqual (id: PeerId | Uint8Array): boolean; - - /** - * Check if this PeerId instance is valid (privKey -> pubKey -> Id) - */ - isValid (): boolean; - - /** - * Check if the PeerId has an inline public key. - */ - hasInlinePublicKey (): boolean; -} - -export = PeerId diff --git a/src/index.js b/src/index.js index d4c7b4f..916765b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,3 @@ -/* - * Id is an object representation of a peer Id. a peer Id is a multihash - */ - 'use strict' const { CID } = require('multiformats/cid') @@ -14,7 +10,6 @@ const { base32 } = require('multiformats/bases/base32') const { base16 } = require('multiformats/bases/base16') const Digest = require('multiformats/hashes/digest') const cryptoKeys = require('libp2p-crypto/src/keys') -const withIs = require('class-is') const { PeerIdProto } = require('./proto') const { equals: uint8ArrayEquals } = require('uint8arrays/equals') const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') @@ -36,7 +31,42 @@ const baseDecoder = Object.keys(bases).reduce( const DAG_PB_CODE = 0x70 const LIBP2P_KEY_CODE = 0x72 +const symbol = Symbol.for('@libp2p/peer-id') + +/** + * @typedef {import('libp2p-crypto').PrivateKey} PrivateKey + * @typedef {import('libp2p-crypto').PublicKey} PublicKey + * @typedef {import('libp2p-crypto').KeyType} KeyType + * @typedef {import('cids').CIDVersion} CIDVersion + */ + +/** + * PeerId JSON format. + * + * @typedef {Object} PeerIdJSON + * @property {string} id - String representation of PeerId. + * @property {string} [pubKey] - Public key. + * @property {string} [privKey] - Private key. + */ + +/** + * Options for PeerId creation. + * + * @typedef {Object} CreateOptions + * @property {number} [bits] - The number of bits to use. + * @property {KeyType} [keyType] - The type of key to use. + */ + class PeerId { + /** + * Create PeerId object + * + * @class + * + * @param {Uint8Array} id + * @param {PrivateKey} [privKey] + * @param {PublicKey} [pubKey] + */ constructor (id, privKey, pubKey) { if (!(id instanceof Uint8Array)) { throw new Error('invalid id provided') @@ -46,20 +76,30 @@ class PeerId { throw new Error('inconsistent arguments') } + // Define symbol + Object.defineProperty(this, symbol, { value: true }) + this._id = id this._idB58String = base58btc.encode(this.id).substring(1) this._privKey = privKey this._pubKey = pubKey } + /** + * @type {Uint8Array} + */ get id () { return this._id } + // @ts-ignore set id (val) { throw new Error('Id is immutable') } + /** + * @type {PrivateKey | undefined} + */ get privKey () { return this._privKey } @@ -68,6 +108,9 @@ class PeerId { this._privKey = privKey } + /** + * @type {PublicKey | undefined} + */ get pubKey () { if (this._pubKey) { return this._pubKey @@ -94,21 +137,34 @@ class PeerId { this._pubKey = pubKey } - // Return the protobuf version of the public key, matching go ipfs formatting + /** + * Return the protobuf version of the public key, matching go ipfs formatting + * + * @returns {Uint8Array | undefined} + */ marshalPubKey () { if (this.pubKey) { return cryptoKeys.marshalPublicKey(this.pubKey) } } - // Return the protobuf version of the private key, matching go ipfs formatting + /** + * Return the protobuf version of the private key, matching go ipfs formatting + * + * @returns {Uint8Array | undefined} + */ marshalPrivKey () { if (this.privKey) { return cryptoKeys.marshalPrivateKey(this.privKey) } } - // Return the protobuf version of the peer-id + /** + * Return the protobuf version of the peer-id + * + * @param {boolean} [excludePriv] - Whether to exclude the private key information from the output. + * @returns {Uint8Array} + */ marshal (excludePriv) { return PeerIdProto.encode({ id: this.toBytes(), @@ -117,6 +173,11 @@ class PeerId { }).finish() } + /** + * String representation + * + * @returns {string} + */ toPrint () { let pid = this.toB58String() // All sha256 nodes start with Qm @@ -132,32 +193,58 @@ class PeerId { return '' } - // return the jsonified version of the key, matching the formatting - // of go-ipfs for its config file + /** + * The jsonified version of the key, matching the formatting of go-ipfs for its config file + * + * @returns {PeerIdJSON} + */ toJSON () { + const privKey = this.marshalPrivKey() + const pubKey = this.marshalPubKey() + return { id: this.toB58String(), - privKey: toB64Opt(this.marshalPrivKey()), - pubKey: toB64Opt(this.marshalPubKey()) + privKey: privKey && toB64Opt(privKey), + pubKey: pubKey && toB64Opt(pubKey) } } - // encode/decode functions + /** + * Encode to hex. + * + * @returns {string} + */ toHexString () { return base16.encode(this.id).substring(1) } + /** + * Return raw id bytes + * + * @returns {Uint8Array} + */ toBytes () { return this.id } + /** + * Encode to base58 string. + * + * @returns {string} + */ toB58String () { return this._idB58String } - // return self-describing String representation - // in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 + /** + * Self-describing String representation + * in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 + * + * @returns {string} + */ toString () { + // We will create the property right here + // @ts-ignore if (!this._idCIDString) { const cid = CID.createV1(LIBP2P_KEY_CODE, Digest.decode(this.id)) @@ -166,6 +253,9 @@ class PeerId { enumerable: false }) } + + // This property is just created + // @ts-ignore return this._idCIDString } @@ -196,14 +286,17 @@ class PeerId { return this.equals(id) } - /* + /** * Check if this PeerId instance is valid (privKey -> pubKey -> Id) + * + * @returns {boolean} */ isValid () { // TODO: needs better checking return Boolean(this.privKey && this.privKey.public && this.privKey.public.bytes && + this.pubKey && this.pubKey.bytes instanceof Uint8Array && uint8ArrayEquals(this.privKey.public.bytes, this.pubKey.bytes)) } @@ -226,202 +319,257 @@ class PeerId { return false } -} -const PeerIdWithIs = withIs(PeerId, { - className: 'PeerId', - symbolName: '@libp2p/js-peer-id/PeerId' -}) - -exports = module.exports = PeerIdWithIs - -const computeDigest = (pubKey) => { - if (pubKey.bytes.length <= 42) { - return Digest.create(identity.code, pubKey.bytes).bytes - } else { - return pubKey.hash() + /** + * Create a new PeerId. + * + * @param {CreateOptions} [opts] - Options + * @returns {Promise} + */ + static async create ({ bits = 2048, keyType = 'RSA' } = {}) { + const key = await cryptoKeys.generateKeyPair(keyType, bits) + return computePeerId(key.public, key) } -} -const computePeerId = async (privKey, pubKey) => { - const digest = await computeDigest(pubKey) - return new PeerIdWithIs(digest, privKey, pubKey) -} - -// generation -exports.create = async (opts) => { - opts = opts || {} - opts.bits = opts.bits || 2048 - opts.keyType = opts.keyType || 'RSA' - - const key = await cryptoKeys.generateKeyPair(opts.keyType, opts.bits) - return computePeerId(key, key.public) -} - -exports.createFromHexString = (str) => { - return new PeerIdWithIs(base16.decode('f' + str)) -} - -exports.createFromBytes = (buf) => { - try { - const cid = CID.decode(buf) - - if (!validMulticodec(cid)) { - throw new Error('Supplied PeerID CID is invalid') - } - - return exports.createFromCID(cid) - } catch { - const digest = Digest.decode(buf) - - if (digest.code !== identity.code) { - throw new Error('Supplied PeerID CID is invalid') - } - - return new PeerIdWithIs(buf) + /** + * Create PeerId from raw bytes. + * + * @param {Uint8Array} buf - The raw bytes. + * @returns {PeerId} + */ + static createFromBytes (buf) { + return new PeerId(buf) } -} -exports.createFromB58String = (str) => { - return exports.createFromBytes(base58btc.decode('z' + str)) -} - -const validMulticodec = (cid) => { - // supported: 'libp2p-key' (CIDv1) and 'dag-pb' (CIDv0 converted to CIDv1) - return cid.code === LIBP2P_KEY_CODE || cid.code === DAG_PB_CODE -} - -exports.createFromCID = (cid) => { - cid = CID.asCID(cid) - - if (!cid || !validMulticodec(cid)) { - throw new Error('Supplied PeerID CID is invalid') + /** + * Create PeerId from base58-encoded string. + * + * @param {string} str - The base58-encoded string. + * @returns {PeerId} + */ + static createFromB58String (str) { + return PeerId.createFromCID(str) // B58String is CIDv0 } - return new PeerIdWithIs(cid.multihash.bytes) -} - -// Public Key input will be a Uint8Array -exports.createFromPubKey = async (key) => { - let buf = key - - if (typeof buf === 'string') { - buf = uint8ArrayFromString(key, 'base64pad') + /** + * Create PeerId from hex string. + * + * @param {string} str - The hex string. + * @returns {PeerId} + */ + static createFromHexString (str) { + return new PeerId(base16.decode('f' + str)) } - if (!(buf instanceof Uint8Array)) { - throw new Error('Supplied key is neither a base64 string nor a Uint8Array') - } + /** + * Create PeerId from CID. + * + * @param {CID | CIDVersion | Uint8Array | string} cid - The CID. + * @returns {PeerId} + */ + static createFromCID (cid) { + cid = CID.asCID(cid) - const pubKey = await cryptoKeys.unmarshalPublicKey(buf) - return computePeerId(undefined, pubKey) -} + if (!cid || !validMulticodec(cid)) { + throw new Error('Supplied PeerID CID is invalid') + } -// Private key input will be a string -exports.createFromPrivKey = async (key) => { - if (typeof key === 'string') { - key = uint8ArrayFromString(key, 'base64pad') + return new PeerId(cid.multihash.bytes) } - if (!(key instanceof Uint8Array)) { - throw new Error('Supplied key is neither a base64 string nor a Uint8Array') - } + /** + * Create PeerId from public key. + * + * @param {Uint8Array | string} key - Public key, as Uint8Array or base64-encoded string. + * @returns {Promise} + */ + static async createFromPubKey (key) { + let buf = key - const privKey = await cryptoKeys.unmarshalPrivateKey(key) - return computePeerId(privKey, privKey.public) -} + if (typeof buf === 'string') { + buf = uint8ArrayFromString(String(key), 'base64pad') + } -exports.createFromJSON = async (obj) => { - const id = base58btc.decode('z' + obj.id) - const rawPrivKey = obj.privKey && uint8ArrayFromString(obj.privKey, 'base64pad') - const rawPubKey = obj.pubKey && uint8ArrayFromString(obj.pubKey, 'base64pad') - const pub = rawPubKey && await cryptoKeys.unmarshalPublicKey(rawPubKey) + if (!(buf instanceof Uint8Array)) { + throw new Error('Supplied key is neither a base64 string nor a Uint8Array') + } - if (!rawPrivKey) { - return new PeerIdWithIs(id, undefined, pub) + const pubKey = await cryptoKeys.unmarshalPublicKey(buf) + return computePeerId(pubKey) } - const privKey = await cryptoKeys.unmarshalPrivateKey(rawPrivKey) - const privDigest = await computeDigest(privKey.public) - - let pubDigest - - if (pub) { - pubDigest = await computeDigest(pub) - } + /** + * Create PeerId from private key. + * + * @param {Uint8Array | string} key - Private key, as Uint8Array or base64-encoded string. + * @returns {Promise} + */ + static async createFromPrivKey (key) { + if (typeof key === 'string') { + key = uint8ArrayFromString(key, 'base64pad') + } - if (pub && !uint8ArrayEquals(privDigest, pubDigest)) { - throw new Error('Public and private key do not match') - } + if (!(key instanceof Uint8Array)) { + throw new Error('Supplied key is neither a base64 string nor a Uint8Array') + } - if (id && !uint8ArrayEquals(privDigest, id)) { - throw new Error('Id and private key do not match') + const privKey = await cryptoKeys.unmarshalPrivateKey(key) + return computePeerId(privKey.public, privKey) } - return new PeerIdWithIs(id, privKey, pub) -} + /** + * Create PeerId from PeerId JSON formatted object. + * + * @param {PeerIdJSON} obj + * @returns {Promise} + */ + static async createFromJSON (obj) { + const id = base58btc.decode('z' + obj.id) + const rawPrivKey = obj.privKey && uint8ArrayFromString(obj.privKey, 'base64pad') + const rawPubKey = obj.pubKey && uint8ArrayFromString(obj.pubKey, 'base64pad') + const pub = rawPubKey && await cryptoKeys.unmarshalPublicKey(rawPubKey) + + if (!rawPrivKey) { + return new PeerId(id, undefined, pub) + } -exports.createFromProtobuf = async (buf) => { - if (typeof buf === 'string') { - buf = uint8ArrayFromString(buf, 'base16') - } + const privKey = await cryptoKeys.unmarshalPrivateKey(rawPrivKey) + const privDigest = await computeDigest(privKey.public) - let { id, privKey, pubKey } = PeerIdProto.decode(buf) + let pubDigest - privKey = privKey ? await cryptoKeys.unmarshalPrivateKey(privKey) : false - pubKey = pubKey ? await cryptoKeys.unmarshalPublicKey(pubKey) : false + if (pub) { + pubDigest = await computeDigest(pub) + } - let pubDigest - let privDigest + if (pub && !uint8ArrayEquals(privDigest, pubDigest)) { + throw new Error('Public and private key do not match') + } - if (privKey) { - privDigest = await computeDigest(privKey.public) - } + if (id && !uint8ArrayEquals(privDigest, id)) { + throw new Error('Id and private key do not match') + } - if (pubKey) { - pubDigest = await computeDigest(pubKey) + return new PeerId(id, privKey, pub) } - if (privKey) { + /** + * Create PeerId from Protobuf bytes. + * + * @param {Uint8Array | string} buf - Protobuf bytes, as Uint8Array or hex-encoded string. + * @returns {Promise} + */ + static async createFromProtobuf (buf) { + if (typeof buf === 'string') { + buf = uint8ArrayFromString(buf, 'base16') + } + + let { id, privKey, pubKey } = PeerIdProto.decode(buf) + + privKey = privKey ? await cryptoKeys.unmarshalPrivateKey(privKey) : false + pubKey = pubKey ? await cryptoKeys.unmarshalPublicKey(pubKey) : false + + let pubDigest + let privDigest + + if (privKey) { + privDigest = await computeDigest(privKey.public) + } + if (pubKey) { - if (!uint8ArrayEquals(privDigest, pubDigest)) { - throw new Error('Public and private key do not match') + pubDigest = await computeDigest(pubKey) + } + + if (privKey) { + if (pubKey) { + if (!uint8ArrayEquals(privDigest, pubDigest)) { + throw new Error('Public and private key do not match') + } } + return new PeerId(privDigest, privKey, privKey.public) + } + + // TODO: val id and pubDigest + + if (pubKey) { + return new PeerId(pubDigest, undefined, pubKey) + } + + if (id) { + return new PeerId(id) } - return new PeerIdWithIs(privDigest, privKey, privKey.public) + + throw new Error('Protobuf did not contain any usable key material') } - // TODO: val id and pubDigest - - if (pubKey) { - return new PeerIdWithIs(pubDigest, undefined, pubKey) + /** + * Parse PeerId object + * + * @param {String} str + * @returns + */ + static parse(str) { + if (str.charAt(0) === '1' || str.charAt(0) === 'Q') { + // identity hash ed25519 key or sha2-256 hash of rsa public key + // base58btc encoded either way + str = `z${str}` + } + + return PeerId.createFromBytes(baseDecoder.decode(str)) } - if (id) { - return new PeerIdWithIs(id) + /** + * Checks if a value is an instance of PeerId. + * + * @param {any} peerId - The value to check. + * @returns {boolean} + */ + static isPeerId (peerId) { + return peerId instanceof PeerId || Boolean(peerId && peerId[symbol]) } - - throw new Error('Protobuf did not contain any usable key material') } -exports.parse = (str) => { - if (str.charAt(0) === '1' || str.charAt(0) === 'Q') { - // identity hash ed25519 key or sha2-256 hash of rsa public key - // base58btc encoded either way - str = `z${str}` +/** + * Compute digest. + * + * @param {PublicKey} pubKey + * @returns {Promise} + */ +const computeDigest = async (pubKey) => { + if (pubKey.bytes.length <= 42) { + return Digest.create(identity.code, pubKey.bytes).bytes + } else { + return pubKey.hash() } +} - return exports.createFromBytes(baseDecoder.decode(str)) +/** + * Compute PeerId. + * + * @param {PublicKey} pubKey + * @param {PrivateKey} [privKey] + * @returns {Promise} + */ +const computePeerId = async (pubKey, privKey) => { + const digest = await computeDigest(pubKey) + return new PeerId(digest, privKey, pubKey) } -exports.isPeerId = (peerId) => { - return Boolean(typeof peerId === 'object' && - peerId._id && - peerId._idB58String) +/** + * Create PeerId from CID. + * + * @param {CID | {codec: string}} cid - The CID. + * @returns {boolean} + */ +const validMulticodec = (cid) => { + // supported: 'libp2p-key' (CIDv1) and 'dag-pb' (CIDv0 converted to CIDv1) + return cid.code === LIBP2P_KEY_CODE || cid.code === DAG_PB_CODE } -function toB64Opt (val) { - if (val) { - return uint8ArrayToString(val, 'base64pad') - } +/** + * @param {Uint8Array} val + * @returns {string} + */ +const toB64Opt = (val) => { + return uint8ArrayToString(val, 'base64pad') }