diff --git a/README.md b/README.md index 96ea07d..e1932b8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ - [`createFromPrivKey(privKey)`](#createfromprivkeyprivkey) - [`createFromJSON(obj)`](#createfromjsonobj) - [`createFromProtobuf(buf)`](#createfromprotobufbuf) + - [`parse(str)`](#parsestr) - [Export](#export) - [`toHexString()`](#tohexstring) - [`toBytes()`](#tobytes) @@ -169,7 +170,7 @@ Returns `PeerId`. ### `createFromCID(cid)` -- `cid: CID|String|Buffer` - The multihash encoded as [CID](https://github.com/ipld/js-cid) (object, `String` or `Buffer`) +- `cid: CID` - The multihash encoded as [CID](https://github.com/multiformats/js-multiformats/blob/master/src/cid.js) object Creates a Peer ID from a CID representation of the key's multihash ([RFC 0001](https://github.com/libp2p/specs/blob/master/RFC/0001-text-peerid-cid.md)). @@ -209,6 +210,10 @@ Returns `Promise`. `buf` is a protocol-buffers encoded PeerId (see `marshal()`) +### `parse(str)` + +Attempts to create a PeerId from a base58btc encoded string or a CID encoded as a string. + ## Export ### `toHexString()` diff --git a/package.json b/package.json index 58692fb..3f6ebfc 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "bin": "src/bin.js", "scripts": { "lint": "aegir lint", + "prepare": "npm run build", "build": "npm run build:proto && npm run build:proto-types && aegir build --no-types", "build:proto": "pbjs -t static-module -w commonjs -r libp2p-peer-id --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/proto.js ./src/proto.proto", "build:proto-types": "pbts -o src/proto.d.ts src/proto.js", @@ -41,14 +42,14 @@ "@types/dirty-chai": "^2.0.2", "@types/mocha": "^8.2.0", "aegir": "^33.0.0", + "multihashes": "^4.0.2", "util": "^0.12.3" }, "dependencies": { - "cids": "^1.1.5", "class-is": "^1.1.0", "libp2p-crypto": "^0.19.0", "minimist": "^1.2.5", - "multihashes": "^4.0.2", + "multiformats": "^9.0.0", "protobufjs": "^6.10.2", "uint8arrays": "^2.0.5" }, diff --git a/src/index.d.ts b/src/index.d.ts index 0906fb3..7de24e5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,5 +1,5 @@ import { PrivateKey, PublicKey, KeyType } from "libp2p-crypto"; -import CID from 'cids' +import { CID } from 'multiformats/cid' declare namespace PeerId { /** @@ -68,7 +68,7 @@ declare namespace PeerId { * Create PeerId from CID. * @param cid The CID. */ - function createFromCID(cid: CID | Uint8Array | string | object): PeerId; + function createFromCID(cid: CID): PeerId; /** * Create PeerId from public key. @@ -94,6 +94,12 @@ declare namespace PeerId { * @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; } /** diff --git a/src/index.js b/src/index.js index 05c8122..b3ee793 100644 --- a/src/index.js +++ b/src/index.js @@ -4,14 +4,21 @@ 'use strict' -const mh = require('multihashes') -const CID = require('cids') +const { CID } = require('multiformats/cid') +const { base58btc } = require('multiformats/bases/base58') +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 uint8ArrayEquals = require('uint8arrays/equals') const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') +const { identity } = require('multiformats/hashes/identity') + +// these values are from https://github.com/multiformats/multicodec/blob/master/table.csv +const DAG_PB_CODE = 0x70 +const LIBP2P_KEY_CODE = 0x72 class PeerId { constructor (id, privKey, pubKey) { @@ -24,7 +31,7 @@ class PeerId { } this._id = id - this._idB58String = mh.toB58String(this.id) + this._idB58String = base58btc.encode(this.id).substring(1) this._privKey = privKey this._pubKey = pubKey } @@ -55,9 +62,9 @@ class PeerId { } try { - const decoded = mh.decode(this.id) + const decoded = Digest.decode(this.id) - if (decoded.name === 'identity') { + if (decoded.code === identity.code) { this._pubKey = cryptoKeys.unmarshalPublicKey(decoded.digest) } } catch (_) { @@ -121,7 +128,7 @@ class PeerId { // encode/decode functions toHexString () { - return mh.toHexString(this.id) + return base16.encode(this.id).substring(1) } toBytes () { @@ -136,10 +143,10 @@ class PeerId { // in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 toString () { if (!this._idCIDString) { - const cid = new CID(1, 'libp2p-key', this.id, 'base32') + const cid = CID.createV1(LIBP2P_KEY_CODE, Digest.decode(this.id)) Object.defineProperty(this, '_idCIDString', { - value: cid.toBaseEncodedString('base32'), + value: cid.toString(), enumerable: false }) } @@ -192,8 +199,9 @@ class PeerId { */ hasInlinePublicKey () { try { - const decoded = mh.decode(this.id) - if (decoded.name === 'identity') { + const decoded = Digest.decode(this.id) + + if (decoded.code === identity.code) { return true } } catch (_) { @@ -213,7 +221,7 @@ exports = module.exports = PeerIdWithIs const computeDigest = (pubKey) => { if (pubKey.bytes.length <= 42) { - return mh.encode(pubKey.bytes, 'identity') + return Digest.create(identity.code, pubKey.bytes).bytes } else { return pubKey.hash() } @@ -235,26 +243,46 @@ exports.create = async (opts) => { } exports.createFromHexString = (str) => { - return new PeerIdWithIs(mh.fromHexString(str)) + return new PeerIdWithIs(base16.decode('f' + str)) } exports.createFromBytes = (buf) => { - return new PeerIdWithIs(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) + } } exports.createFromB58String = (str) => { - return exports.createFromCID(str) // B58String is CIDv0 + return exports.createFromBytes(base58btc.decode('z' + str)) } const validMulticodec = (cid) => { // supported: 'libp2p-key' (CIDv1) and 'dag-pb' (CIDv0 converted to CIDv1) - return cid.codec === 'libp2p-key' || cid.codec === 'dag-pb' + return cid.code === LIBP2P_KEY_CODE || cid.code === DAG_PB_CODE } exports.createFromCID = (cid) => { - cid = CID.isCID(cid) ? cid : new CID(cid) - if (!validMulticodec(cid)) throw new Error('Supplied PeerID CID has invalid multicodec: ' + cid.codec) - return new PeerIdWithIs(cid.multihash) + cid = CID.asCID(cid) + + if (!cid || !validMulticodec(cid)) { + throw new Error('Supplied PeerID CID is invalid') + } + + return new PeerIdWithIs(cid.multihash.bytes) } // Public Key input will be a Uint8Array @@ -288,7 +316,7 @@ exports.createFromPrivKey = async (key) => { } exports.createFromJSON = async (obj) => { - const id = mh.fromB58String(obj.id) + 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) @@ -360,6 +388,16 @@ exports.createFromProtobuf = async (buf) => { throw new Error('Protobuf did not contain any usable key material') } +exports.parse = (str) => { + if (str.charAt(0) === '1') { + // base58btc encoded public key + return exports.createFromBytes(base58btc.decode(`z${str}`)) + } + + // try to parse it as a regular base58btc multihash or base32 encoded CID + return exports.createFromCID(CID.parse(str)) +} + exports.isPeerId = (peerId) => { return Boolean(typeof peerId === 'object' && peerId._id && diff --git a/test/peer-id.spec.js b/test/peer-id.spec.js index 4d4720e..36beb7e 100644 --- a/test/peer-id.spec.js +++ b/test/peer-id.spec.js @@ -5,7 +5,8 @@ const { expect } = require('aegir/utils/chai') const crypto = require('libp2p-crypto') const mh = require('multihashes') -const CID = require('cids') +const { CID } = require('multiformats/cid') +const Digest = require('multiformats/hashes/digest') const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') @@ -13,12 +14,17 @@ const PeerId = require('../src') const util = require('util') +const DAG_PB_CODE = 0x70 +const LIBP2P_KEY_CODE = 0x72 +const RAW_CODE = 0x55 + const testId = require('./fixtures/sample-id') const testIdHex = testId.id const testIdBytes = mh.fromHexString(testId.id) +const testIdDigest = Digest.decode(testIdBytes) const testIdB58String = mh.toB58String(testIdBytes) -const testIdCID = new CID(1, 'libp2p-key', testIdBytes) -const testIdCIDString = testIdCID.toBaseEncodedString('base32') +const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) +const testIdCIDString = testIdCID.toString() const goId = require('./fixtures/go-private-key') @@ -90,63 +96,55 @@ describe('PeerId', () => { }) it('recreate from Base58 String (CIDv0))', () => { - const id = PeerId.createFromCID(testIdB58String) + const id = PeerId.createFromCID(CID.parse(testIdB58String)) expect(testIdCIDString).to.equal(id.toString()) expect(testIdBytes).to.deep.equal(id.toBytes()) }) it('recreate from CIDv1 Base32 (libp2p-key multicodec)', () => { - const cid = new CID(1, 'libp2p-key', testIdBytes) - const cidString = cid.toBaseEncodedString('base32') - const id = PeerId.createFromCID(cidString) - expect(cidString).to.equal(id.toString()) + const cid = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) + const id = PeerId.createFromCID(cid) + expect(cid.toString()).to.equal(id.toString()) expect(testIdBytes).to.deep.equal(id.toBytes()) }) it('recreate from CIDv1 Base32 (dag-pb multicodec)', () => { - const cid = new CID(1, 'dag-pb', testIdBytes) - const cidString = cid.toBaseEncodedString('base32') - const id = PeerId.createFromCID(cidString) + const cid = CID.createV1(DAG_PB_CODE, testIdDigest) + const id = PeerId.createFromCID(cid) // toString should return CID with multicodec set to libp2p-key - expect(new CID(id.toString()).codec).to.equal('libp2p-key') + expect(CID.parse(id.toString()).code).to.equal(LIBP2P_KEY_CODE) expect(testIdBytes).to.deep.equal(id.toBytes()) }) it('recreate from CID Uint8Array', () => { - const id = PeerId.createFromCID(testIdCID.bytes) + const id = PeerId.createFromBytes(testIdCID.bytes) expect(testIdCIDString).to.equal(id.toString()) expect(testIdBytes).to.deep.equal(id.toBytes()) }) it('throws on invalid CID multicodec', () => { // only libp2p and dag-pb are supported - const invalidCID = new CID(1, 'raw', testIdBytes).toBaseEncodedString('base32') + const invalidCID = CID.createV1(RAW_CODE, testIdDigest) expect(() => { PeerId.createFromCID(invalidCID) - }).to.throw(/Supplied PeerID CID has invalid multicodec: raw/) + }).to.throw(/invalid/i) }) - it('throws on invalid CID value', () => { - // using function code that does not represent valid hash function + it('throws on invalid multihash value', () => { + // using function code 0x50 that does not represent valid hash function // https://github.com/multiformats/js-multihash/blob/b85999d5768bf06f1b0f16b926ef2cb6d9c14265/src/constants.js#L345 - const invalidCID = 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L' + const invalidMultihash = uint8ArrayToString(Uint8Array.from([0x50, 0x1, 0x0]), 'base58btc') expect(() => { - PeerId.createFromCID(invalidCID) - }).to.throw(/multihash unknown function code: 0x50/) + PeerId.createFromB58String(invalidMultihash) + }).to.throw(/invalid/i) }) it('throws on invalid CID object', () => { const invalidCID = {} expect(() => { + // @ts-expect-error invalid cid is invalid type PeerId.createFromCID(invalidCID) - }).to.throw(/Invalid version, must be a number equal to 1 or 0/) - }) - - it('throws on invalid CID object', () => { - const invalidCID = {} - expect(() => { - PeerId.createFromCID(invalidCID) - }).to.throw(/Invalid version, must be a number equal to 1 or 0/) + }).to.throw(/invalid/i) }) it('recreate from a Public Key', async () => { @@ -174,6 +172,28 @@ describe('PeerId', () => { expect(uint8ArrayToString(id.marshal(), 'base16')).to.deep.equal(testId.marshaled) }) + it('recreate from embedded ed25519 key', async () => { + const key = '12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD' + const id = await PeerId.parse(key) + expect(id.toB58String()).to.equal(key) + const expB58 = mh.toB58String(mh.encode(id.pubKey.bytes, 'identity')) + expect(id.toB58String()).to.equal(expB58) + }) + + it('recreate from embedded secp256k1 key', async () => { + const key = '16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E' + const id = await PeerId.parse(key) + expect(id.toB58String()).to.equal(key) + const expB58 = mh.toB58String(mh.encode(id.pubKey.bytes, 'identity')) + expect(id.toB58String()).to.equal(expB58) + }) + + it('recreate from string key', async () => { + const key = 'QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC' + const id = await PeerId.parse(key) + expect(id.toB58String()).to.equal(key) + }) + it('can be created from a Secp256k1 public key', async () => { const privKey = await crypto.keys.generateKeyPair('secp256k1', 256) const id = await PeerId.createFromPubKey(privKey.public.bytes) @@ -209,7 +229,8 @@ describe('PeerId', () => { it('Pretty printing', async () => { const id1 = await PeerId.create(testOpts) - const id2 = await PeerId.createFromPrivKey((id1.toJSON()).privKey) + const json = id1.toJSON() + const id2 = await PeerId.createFromPrivKey(json.privKey || 'invalid, should not happen') expect(id1.toPrint()).to.be.eql(id2.toPrint()) expect(id1.toPrint()).to.equal('') }) @@ -375,6 +396,7 @@ describe('PeerId', () => { }) it('invalid id', () => { + // @ts-expect-error incorrect constructor arg type expect(() => new PeerId('hello world')).to.throw(/invalid id/) }) })