diff --git a/.aegir.js b/.aegir.js index 08fb705e8e..18d6236a58 100644 --- a/.aegir.js +++ b/.aegir.js @@ -45,7 +45,7 @@ const after = async () => { } module.exports = { - bundlesize: { maxSize: '185kB' }, + bundlesize: { maxSize: '200kB' }, hooks: { pre: before, post: after diff --git a/doc/API.md b/doc/API.md index f5efeb3978..91a2511d69 100644 --- a/doc/API.md +++ b/doc/API.md @@ -41,6 +41,17 @@ * [`connectionManager.get`](#connectionmanagerget) * [`connectionManager.setPeerValue`](#connectionmanagersetpeervalue) * [`connectionManager.size`](#connectionmanagersize) + * [`keychain.createKey`](#keychaincreatekey) + * [`keychain.renameKey`](#keychainrenamekey) + * [`keychain.removeKey`](#keychainremovekey) + * [`keychain.exportKey`](#keychainexportkey) + * [`keychain.importKey`](#keychainimportkey) + * [`keychain.importPeer`](#keychainimportpeer) + * [`keychain.list`](#keychainlist) + * [`keychain.findById`](#keychainfindbyid) + * [`keychain.findByName`](#keychainfindbyname) + * [`keychain.cms.encrypt`](#keychaincmsencrypt) + * [`keychain.cms.decrypt`](#keychaincmsdecrypt) * [`metrics.global`](#metricsglobal) * [`metrics.peers`](#metricspeers) * [`metrics.protocols`](#metricsprotocols) @@ -72,7 +83,8 @@ Creates an instance of Libp2p. | [options.connectionManager] | `object` | libp2p Connection Manager configuration | | [options.datastore] | `object` | must implement [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore) (in memory datastore will be used if not provided) | | [options.dialer] | `object` | libp2p Dialer configuration -| [options.metrics] | `object` | libp2p Metrics configuration +| [options.keychain] | [`object`](./CONFIGURATION.md#setup-with-keychain) | libp2p Keychain configuration | +| [options.metrics] | `object` | libp2p Metrics configuration | | [options.peerId] | [`PeerId`][peer-id] | peerId instance (it will be created if not provided) | | [options.peerStore] | `object` | libp2p PeerStore configuration | @@ -1227,6 +1239,278 @@ libp2p.connectionManager.size // 10 ``` +### keychain.createKey + +Create a key in the keychain. + +`libp2p.keychain.createKey(name, type, size)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. It cannot already exist. | +| type | `string` | One of the key types; 'rsa' | +| size | `number` | The key size in bits. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +const keyInfo = await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +``` + +### keychain.renameKey + +Rename a key in the keychain. + +`libp2p.keychain.renameKey(oldName, newName)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The old local key name. It must already exist. | +| type | `string` | The new local key name. It must not already exist. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') +``` + +### keychain.removeKey + +Removes a key from the keychain. + +`libp2p.keychain.removeKey(name)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. It must already exist. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const keyInfo = await libp2p.keychain.removeKey('keyTest') +``` + +### keychain.exportKey + +Export an existing key as a PEM encrypted PKCS #8 string. + +`libp2p.keychain.exportKey(name, password)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. It must already exist. | +| password | `string` | The password to use. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Key as a PEM encrypted PKCS #8 | + +#### Example + +```js +await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') +``` + +### keychain.importKey + +Import a new key from a PEM encoded PKCS #8 string. + +`libp2p.keychain.importKey(name, pem, password)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. It must not exist. | +| pem | `string` | The PEM encoded PKCS #8 string. | +| password | `string` | The password to use. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') +const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') +``` + +### keychain.importPeer + +Import a new key from a PeerId. + +`libp2p.keychain.importPeer(name, peerId)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. It must not exist. | +| peerId | ['PeerId'][peer-id] | The PEM encoded PKCS #8 string. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerId) +``` + +### keychain.list + +List all the keys. + +`libp2p.keychain.list()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise>` | Array of Key info | + +#### Example + +```js +const keyInfos = await libp2p.keychain.list() +``` + +### keychain.findById + +Find a key by it's id. + +`libp2p.keychain.findById(id)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| id | `string` | The universally unique key identifier. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +const keyInfo = await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const keyInfo2 = await libp2p.keychain.findById(keyInfo.id) +``` + +### keychain.findByName + +Find a key by it's name. + +`libp2p.keychain.findByName(id)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| id | `string` | The local key name. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ id, name }>` | Key info object | + +#### Example + +```js +const keyInfo = await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const keyInfo2 = await libp2p.keychain.findByName('keyTest') +``` + +### keychain.cms.encrypt + +Encrypt protected data using the Cryptographic Message Syntax (CMS). + +`libp2p.keychain.cms.encrypt(name, data)` + +| Name | Type | Description | +|------|------|-------------| +| name | `string` | The local key name. | +| data | `Buffer` | The data to encrypt. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Encrypted data as a PKCS #7 message in DER. | + +#### Example + +```js +const keyInfo = await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const enc = await libp2p.keychain.cms.encrypt('keyTest', Buffer.from('data')) +``` + +### keychain.cms.decrypt + +Decrypt protected data using the Cryptographic Message Syntax (CMS). +The keychain must contain one of the keys used to encrypt the data. If none of the keys exists, an Error is returned with the property 'missingKeys'. + +`libp2p.keychain.cms.decrypt(cmsData)` + +| Name | Type | Description | +|------|------|-------------| +| cmsData | `string` | The CMS encrypted data to decrypt. | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Decrypted data. | + +#### Example + +```js +const keyInfo = await libp2p.keychain.createKey('keyTest', 'rsa', 4096) +const enc = await libp2p.keychain.cms.encrypt('keyTest', Buffer.from('data')) +``` + ### metrics.global A [`Stats`](#stats) object of tracking the global bandwidth of the libp2p node. diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 833b4ac0b1..b9598d069b 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -20,6 +20,7 @@ - [Customizing DHT](#customizing-dht) - [Setup with Content and Peer Routing](#setup-with-content-and-peer-routing) - [Setup with Relay](#setup-with-relay) + - [Setup with Keychain](#setup-with-keychain) - [Configuring Dialing](#configuring-dialing) - [Configuring Connection Manager](#configuring-connection-manager) - [Configuring Metrics](#configuring-metrics) @@ -422,6 +423,35 @@ const node = await Libp2p.create({ }) ``` +#### Setup with Keychain + +Libp2p allows you to setup a secure key chain to manage your keys. The keychain configuration object should have the following properties: + +| Name | Type | Description | +|------|------|-------------| +| pass | `string` | Passphrase to use in the keychain (minimum of 20 characters). | +| datastore | `object` | must implement [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore) | + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const LevelStore = require('datastore-level') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] + }, + keychain: { + pass: 'notsafepassword123456789', + datastore: new LevelStore('path/to/store') + } +}) +``` + #### Configuring Dialing Dialing in libp2p can be configured to limit the rate of dialing, and how long dials are allowed to take. The below configuration example shows the default values for the dialer. diff --git a/package.json b/package.json index 1f9acca599..cff39b69c0 100644 --- a/package.json +++ b/package.json @@ -67,12 +67,14 @@ "multiaddr": "^7.4.3", "multistream-select": "^0.15.0", "mutable-proxy": "^1.0.0", + "node-forge": "^0.9.1", "p-any": "^3.0.0", "p-fifo": "^1.0.0", "p-settle": "^4.0.1", "peer-id": "^0.13.11", "protons": "^1.0.1", "retimer": "^2.0.0", + "sanitize-filename": "^1.6.3", "streaming-iterables": "^4.1.0", "timeout-abort-controller": "^1.0.0", "xsalsa20": "^1.0.2" @@ -95,6 +97,7 @@ "it-concat": "^1.0.0", "it-pair": "^1.0.0", "it-pushable": "^1.4.0", + "level": "^6.0.1", "libp2p-bootstrap": "^0.11.0", "libp2p-delegated-content-routing": "^0.5.0", "libp2p-delegated-peer-routing": "^0.5.0", @@ -102,12 +105,13 @@ "libp2p-gossipsub": "^0.4.0", "libp2p-kad-dht": "^0.19.1", "libp2p-mdns": "^0.14.1", - "libp2p-noise": "^1.1.0", "libp2p-mplex": "^0.9.5", + "libp2p-noise": "^1.1.0", "libp2p-secio": "^0.12.4", "libp2p-tcp": "^0.14.1", "libp2p-webrtc-star": "^0.18.0", "libp2p-websockets": "^0.13.1", + "multihashes": "^0.4.19", "nock": "^12.0.3", "p-defer": "^3.0.0", "p-times": "^3.0.0", diff --git a/src/index.js b/src/index.js index 6c4bab3843..0050e16e0c 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ const globalThis = require('ipfs-utils/src/globalthis') const log = debug('libp2p') log.error = debug('libp2p:error') +const { MemoryDatastore } = require('interface-datastore') const PeerId = require('peer-id') const peerRouting = require('./peer-routing') @@ -19,6 +20,8 @@ const AddressManager = require('./address-manager') const ConnectionManager = require('./connection-manager') const Circuit = require('./circuit') const Dialer = require('./dialer') +const Keychain = require('./keychain') +const NoKeychain = require('./keychain/no-keychain') const Metrics = require('./metrics') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') @@ -44,8 +47,9 @@ class Libp2p extends EventEmitter { // and add default values where appropriate this._options = validateConfig(_options) - this.peerId = this._options.peerId this.datastore = this._options.datastore + this.keychain = this._options.keychain + this.peerId = this._options.peerId this.peerStore = (this.datastore && this._options.peerStore.persistence) ? new PersistentPeerStore({ @@ -168,6 +172,9 @@ class Libp2p extends EventEmitter { this.peerRouting = peerRouting(this) this.contentRouting = contentRouting(this) + // Keychain + this.keychain = this._options._keychain || new NoKeychain() + // Mount default protocols ping.mount(this) @@ -541,13 +548,40 @@ class Libp2p extends EventEmitter { * @returns {Libp2p} */ Libp2p.create = async function create (options = {}) { - if (options.peerId) { - return new Libp2p(options) + let peerId = options.peerId + + if (!peerId) { + peerId = await PeerId.create() + + options.peerId = peerId } - const peerId = await PeerId.create() + const keychainOptions = options.keychain || {} + + if (keychainOptions.pass) { + log('creating keychain') + + const datastore = keychainOptions.datastore || new MemoryDatastore() + const keychainOpts = Keychain.generateOptions() + + const keychain = new Keychain(datastore, { + passPhrase: keychainOptions.pass, + ...keychainOpts, + ...keychainOptions + }) + + log('keychain constructed') + + // Import the private key as 'self', if needed. + try { + await keychain.findByName('self') + } catch (err) { + await keychain.importPeer('self', peerId) + } + + options._keychain = keychain + } - options.peerId = peerId return new Libp2p(options) } diff --git a/src/keychain/README.md b/src/keychain/README.md index 37829b48a7..1a5a6ce387 100644 --- a/src/keychain/README.md +++ b/src/keychain/README.md @@ -1,20 +1,7 @@ # js-libp2p-keychain -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) -[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) -[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) -[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) -[![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-keychain) -[![](https://img.shields.io/travis/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-keychain) -[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-keychain) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) - > A secure key chain for libp2p in JavaScript -## Lead Maintainer - -[Vasco Santos](https://github.com/vasco-santos). - ## Features - Manages the lifecycle of a key @@ -26,49 +13,6 @@ - Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages - Delays reporting errors to slow down brute force attacks -## Table of Contents - -## Install - -```sh -npm install --save libp2p-keychain -``` - -### Usage - -```js -const Keychain = require('libp2p-keychain') -const FsStore = require('datastore-fs') - -const datastore = new FsStore('./a-keystore') -const opts = { - passPhrase: 'some long easily remembered phrase' -} -const keychain = new Keychain(datastore, opts) -``` - -## API - -Managing a key - -- `async createKey (name, type, size)` -- `async renameKey (oldName, newName)` -- `async removeKey (name)` -- `async exportKey (name, password)` -- `async importKey (name, pem, password)` -- `async importPeer (name, peer)` - -A naming service for a key - -- `async listKeys ()` -- `async findKeyById (id)` -- `async findKeyByName (name)` - -Cryptographically protected messages - -- `async cms.encrypt (name, plain)` -- `async cms.decrypt (cmsData)` - ### KeyInfo The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain. @@ -109,15 +53,3 @@ The actual physical storage of an encrypted key is left to implementations of [i ### Cryptographic Message Syntax (CMS) CMS, aka [PKCS #7](https://en.wikipedia.org/wiki/PKCS) and [RFC 5652](https://tools.ietf.org/html/rfc5652), describes an encapsulation syntax for data protection. It is used to digitally sign, digest, authenticate, or encrypt arbitrary message content. Basically, `cms.encrypt` creates a DER message that can be only be read by someone holding the private key. - -## Contribute - -Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-keychain/issues)! - -This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) - -## License - -[MIT](LICENSE) diff --git a/src/keychain/cms.js b/src/keychain/cms.js index 9bec4b9451..ea35a511a9 100644 --- a/src/keychain/cms.js +++ b/src/keychain/cms.js @@ -43,7 +43,7 @@ class CMS { throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS') } - const key = await this.keychain.findKeyByName(name) + const key = await this.keychain.findByName(name) const pem = await this.keychain._getPrivateKey(name) const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._()) const certificate = await certificateForKey(key, privateKey) @@ -96,7 +96,7 @@ class CMS { const r = await findAsync(recipients, async (recipient) => { try { - const key = await this.keychain.findKeyById(recipient.keyId) + const key = await this.keychain.findById(recipient.keyId) if (key) return true } catch (err) { return false @@ -111,7 +111,7 @@ class CMS { }) } - const key = await this.keychain.findKeyById(r.keyId) + const key = await this.keychain.findById(r.keyId) const pem = await this.keychain._getPrivateKey(key.name) const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._()) cms.decrypt(r.recipient, privateKey) diff --git a/src/keychain/keychain.js b/src/keychain/keychain.js index aae7897224..77f2b2cd20 100644 --- a/src/keychain/keychain.js +++ b/src/keychain/keychain.js @@ -104,29 +104,29 @@ class Keychain { } this.store = store - const opts = mergeOptions(defaultOptions, options) + this.opts = mergeOptions(defaultOptions, options) // Enforce NIST SP 800-132 - if (!opts.passPhrase || opts.passPhrase.length < 20) { + if (!this.opts.passPhrase || this.opts.passPhrase.length < 20) { throw new Error('passPhrase must be least 20 characters') } - if (opts.dek.keyLength < NIST.minKeyLength) { + if (this.opts.dek.keyLength < NIST.minKeyLength) { throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) } - if (opts.dek.salt.length < NIST.minSaltLength) { + if (this.opts.dek.salt.length < NIST.minSaltLength) { throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) } - if (opts.dek.iterationCount < NIST.minIterationCount) { + if (this.opts.dek.iterationCount < NIST.minIterationCount) { throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) } // Create the derived encrypting key const dek = crypto.pbkdf2( - opts.passPhrase, - opts.dek.salt, - opts.dek.iterationCount, - opts.dek.keyLength, - opts.dek.hash) + this.opts.passPhrase, + this.opts.dek.salt, + this.opts.dek.iterationCount, + this.opts.dek.keyLength, + this.opts.dek.hash) Object.defineProperty(this, '_', { value: () => dek }) } @@ -229,7 +229,7 @@ class Keychain { * * @returns {KeyInfo[]} */ - async listKeys () { + async list () { const self = this const query = { prefix: infoPrefix @@ -249,9 +249,9 @@ class Keychain { * @param {string} id - The universally unique key identifier. * @returns {KeyInfo} */ - async findKeyById (id) { + async findById (id) { try { - const keys = await this.listKeys() + const keys = await this.list() return keys.find((k) => k.id === id) } catch (err) { return throwDelayed(err) @@ -264,7 +264,7 @@ class Keychain { * @param {string} name - The local key name. * @returns {KeyInfo} */ - async findKeyByName (name) { + async findByName (name) { if (!validateKeyName(name)) { return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } @@ -290,7 +290,7 @@ class Keychain { return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } const dsname = DsName(name) - const keyInfo = await self.findKeyByName(name) + const keyInfo = await self.findByName(name) const batch = self.store.batch() batch.delete(dsname) batch.delete(DsInfoName(name)) diff --git a/src/keychain/no-keychain.js b/src/keychain/no-keychain.js new file mode 100644 index 0000000000..6e4e612a57 --- /dev/null +++ b/src/keychain/no-keychain.js @@ -0,0 +1,33 @@ +'use strict' + +function fail () { + throw new Error('Key management requires \'--pass ...\' option') +} + +class NoKeychain { + static get options () { fail() } + + static generateOptions () { fail() } + + createKey () { fail() } + + listKeys () { fail() } + + findKeyById () { fail() } + + findKeyByName () { fail() } + + renameKey () { fail() } + + removeKey () { fail() } + + exportKey () { fail() } + + importKey () { fail() } + + importPeer () { fail() } + + get cms () { fail() } +} + +module.exports = NoKeychain diff --git a/test/keychain/keychain-api.spec.js b/test/keychain/keychain-api.spec.js new file mode 100644 index 0000000000..0888427142 --- /dev/null +++ b/test/keychain/keychain-api.spec.js @@ -0,0 +1,394 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const fail = expect.fail +chai.use(require('dirty-chai')) +chai.use(require('chai-string')) + +const os = require('os') +const path = require('path') +const { isBrowser } = require('ipfs-utils/src/env') +const FsStore = require('datastore-fs') +const LevelStore = require('datastore-level') +const Keychain = require('../../src/keychain') +const PeerId = require('peer-id') + +describe('keychain api', () => { + const passPhrase = 'this is not a secure phrase' + const rsaKeyName = 'tajné jméno' + const renamedRsaKeyName = 'ชื่อลับ' + let rsaKeyInfo + let emptyKeystore + let ks + let datastore1, datastore2 + + before(() => { + datastore1 = isBrowser + ? new LevelStore('test-keystore-1', { db: require('level') }) + : new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())) + datastore2 = isBrowser + ? new LevelStore('test-keystore-2', { db: require('level') }) + : new FsStore(path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())) + + ks = new Keychain(datastore2, { passPhrase: passPhrase }) + emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) + }) + + it('needs a pass phrase to encrypt a key', () => { + expect(() => new Keychain(datastore2)).to.throw() + }) + + it('needs a NIST SP 800-132 non-weak pass phrase', () => { + expect(() => new Keychain(datastore2, { passPhrase: '< 20 character' })).to.throw() + }) + + it('needs a store to persist a key', () => { + expect(() => new Keychain(null, { passPhrase: passPhrase })).to.throw() + }) + + it('has default options', () => { + expect(Keychain.options).to.exist() + }) + + it('needs a supported hashing alorithm', () => { + const ok = new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'sha2-256' } }) + expect(ok).to.exist() + expect(() => new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'my-hash' } })).to.throw() + }) + + it('can generate options', () => { + const options = Keychain.generateOptions() + options.passPhrase = passPhrase + const chain = new Keychain(datastore2, options) + expect(chain).to.exist() + }) + + describe('key name', () => { + it('is a valid filename and non-ASCII', async () => { + const errors = await Promise.all([ + ks.removeKey('../../nasty').then(fail, err => err), + ks.removeKey('').then(fail, err => err), + ks.removeKey(' ').then(fail, err => err), + ks.removeKey(null).then(fail, err => err), + ks.removeKey(undefined).then(fail, err => err) + ]) + + expect(errors).to.have.length(5) + errors.forEach(error => { + expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + }) + + describe('key', () => { + it('can be an RSA key', async () => { + rsaKeyInfo = await ks.createKey(rsaKeyName, 'rsa', 2048) + expect(rsaKeyInfo).to.exist() + expect(rsaKeyInfo).to.have.property('name', rsaKeyName) + expect(rsaKeyInfo).to.have.property('id') + }) + + it('is encrypted PEM encoded PKCS #8', async () => { + const pem = await ks._getPrivateKey(rsaKeyName) + return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('throws if an invalid private key name is given', async () => { + const err = await ks._getPrivateKey(undefined).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('throws if a private key cant be found', async () => { + const err = await ks._getPrivateKey('not real').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('does not overwrite existing key', async () => { + const err = await ks.createKey(rsaKeyName, 'rsa', 2048).then(fail, err => err) + expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + const err = await ks.createKey('self', 'rsa', 2048).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate name is string', async () => { + const err = await ks.createKey(5, 'rsa', 2048).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate type is string', async () => { + const err = await ks.createKey('TEST' + Date.now(), null, 2048).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_TYPE') + }) + + it('should validate size is integer', async () => { + const err = await ks.createKey('TEST' + Date.now(), 'rsa', 'string').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') + }) + + describe('implements NIST SP 800-131A', () => { + it('disallows RSA length < 2048', async () => { + const err = await ks.createKey('bad-nist-rsa', 'rsa', 1024).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') + }) + }) + }) + + describe('query', () => { + it('finds all existing keys', async () => { + const keys = await ks.list() + expect(keys).to.exist() + const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) + expect(mykey).to.exist() + }) + + it('finds a key by name', async () => { + const key = await ks.findByName(rsaKeyName) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('finds a key by id', async () => { + const key = await ks.findById(rsaKeyInfo.id) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('returns the key\'s name and id', async () => { + const keys = await ks.list() + expect(keys).to.exist() + keys.forEach((key) => { + expect(key).to.have.property('name') + expect(key).to.have.property('id') + }) + }) + }) + + describe('CMS protected data', () => { + const plainData = Buffer.from('This is a message from Alice to Bob') + let cms + + it('service is available', () => { + expect(ks).to.have.property('cms') + }) + + it('requires a key', async () => { + const err = await ks.cms.encrypt('no-key', plainData).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('requires plain data as a Buffer', async () => { + const err = await ks.cms.encrypt(rsaKeyName, 'plain data').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_PARAMS') + }) + + it('encrypts', async () => { + cms = await ks.cms.encrypt(rsaKeyName, plainData) + expect(cms).to.exist() + expect(cms).to.be.instanceOf(Buffer) + }) + + it('is a PKCS #7 message', async () => { + const err = await ks.cms.decrypt('not CMS').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_PARAMS') + }) + + it('is a PKCS #7 binary message', async () => { + const err = await ks.cms.decrypt(plainData).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_CMS') + }) + + it('cannot be read without the key', async () => { + const err = await emptyKeystore.cms.decrypt(cms).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('missingKeys') + expect(err.missingKeys).to.eql([rsaKeyInfo.id]) + expect(err).to.have.property('code', 'ERR_MISSING_KEYS') + }) + + it('can be read with the key', async () => { + const plain = await ks.cms.decrypt(cms) + expect(plain).to.exist() + expect(plain.toString()).to.equal(plainData.toString()) + }) + }) + + describe('exported key', () => { + let pemKey + + it('requires the password', async () => { + const err = await ks.exportKey(rsaKeyName).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_PASSWORD_REQUIRED') + }) + + it('requires the key name', async () => { + const err = await ks.exportKey(undefined, 'password').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('is a PKCS #8 encrypted pem', async () => { + pemKey = await ks.exportKey(rsaKeyName, 'password') + expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('can be imported', async () => { + const key = await ks.importKey('imported-key', pemKey, 'password') + expect(key.name).to.equal('imported-key') + expect(key.id).to.equal(rsaKeyInfo.id) + }) + + it('requires the pem', async () => { + const err = await ks.importKey('imported-key', undefined, 'password').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_PEM_REQUIRED') + }) + + it('cannot be imported as an existing key name', async () => { + const err = await ks.importKey(rsaKeyName, pemKey, 'password').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot be imported with the wrong password', async () => { + const err = await ks.importKey('a-new-name-for-import', pemKey, 'not the password').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_CANNOT_READ_KEY') + }) + }) + + describe('peer id', () => { + const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' + let alice + + before(async function () { + const encoded = Buffer.from(alicePrivKey, 'base64') + alice = await PeerId.createFromPrivKey(encoded) + }) + + it('private key can be imported', async () => { + const key = await ks.importPeer('alice', alice) + expect(key.name).to.equal('alice') + expect(key.id).to.equal(alice.toB58String()) + }) + + it('private key import requires a valid name', async () => { + const err = await ks.importPeer(undefined, alice).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('private key import requires the peer', async () => { + const err = await ks.importPeer('alice').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_MISSING_PRIVATE_KEY') + }) + + it('key id exists', async () => { + const key = await ks.findById(alice.toB58String()) + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toB58String()) + }) + + it('key name exists', async () => { + const key = await ks.findByName('alice') + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toB58String()) + }) + }) + + describe('rename', () => { + it('requires an existing key name', async () => { + const err = await ks.renameKey('not-there', renamedRsaKeyName).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_NOT_FOUND') + }) + + it('requires a valid new key name', async () => { + const err = await ks.renameKey(rsaKeyName, '..\not-valid').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('does not overwrite existing key', async () => { + const err = await ks.renameKey(rsaKeyName, rsaKeyName).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + const err = await ks.renameKey(rsaKeyName, 'self').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('removes the existing key name', async () => { + const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + // Try to find the changed key + const err = await ks.findByName(rsaKeyName).then(fail, err => err) + expect(err).to.exist() + }) + + it('creates the new key name', async () => { + const key = await ks.findByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + }) + + it('does not change the key ID', async () => { + const key = await ks.findByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + + it('throws with invalid key names', async () => { + const err = await ks.findByName(undefined).then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + + describe('key removal', () => { + it('cannot remove the "self" key', async () => { + const err = await ks.removeKey('self').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('cannot remove an unknown key', async () => { + const err = await ks.removeKey('not-there').then(fail, err => err) + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('can remove a known key', async () => { + const key = await ks.removeKey(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + }) +}) diff --git a/test/keychain/keychain.spec.js b/test/keychain/keychain.spec.js index 9e40797b02..9b3cfc1adb 100644 --- a/test/keychain/keychain.spec.js +++ b/test/keychain/keychain.spec.js @@ -1,394 +1,38 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ 'use strict' +/* eslint-env mocha */ const chai = require('chai') -const expect = chai.expect -const fail = expect.fail chai.use(require('dirty-chai')) -chai.use(require('chai-string')) - -const os = require('os') -const path = require('path') -const { isBrowser } = require('ipfs-utils/src/env') -const FsStore = require('datastore-fs') -const LevelStore = require('datastore-level') -const Keychain = require('../../src/keychain') -const PeerId = require('peer-id') - -describe('keychain', () => { - const passPhrase = 'this is not a secure phrase' - const rsaKeyName = 'tajné jméno' - const renamedRsaKeyName = 'ชื่อลับ' - let rsaKeyInfo - let emptyKeystore - let ks - let datastore1, datastore2 - - before(() => { - datastore1 = isBrowser - ? new LevelStore('test-keystore-1', { db: require('level') }) - : new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())) - datastore2 = isBrowser - ? new LevelStore('test-keystore-2', { db: require('level') }) - : new FsStore(path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())) - - ks = new Keychain(datastore2, { passPhrase: passPhrase }) - emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) - }) - - it('needs a pass phrase to encrypt a key', () => { - expect(() => new Keychain(datastore2)).to.throw() - }) - - it('needs a NIST SP 800-132 non-weak pass phrase', () => { - expect(() => new Keychain(datastore2, { passPhrase: '< 20 character' })).to.throw() - }) - - it('needs a store to persist a key', () => { - expect(() => new Keychain(null, { passPhrase: passPhrase })).to.throw() - }) - - it('has default options', () => { - expect(Keychain.options).to.exist() - }) - - it('needs a supported hashing alorithm', () => { - const ok = new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'sha2-256' } }) - expect(ok).to.exist() - expect(() => new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'my-hash' } })).to.throw() - }) - - it('can generate options', () => { - const options = Keychain.generateOptions() - options.passPhrase = passPhrase - const chain = new Keychain(datastore2, options) - expect(chain).to.exist() - }) - - describe('key name', () => { - it('is a valid filename and non-ASCII', async () => { - const errors = await Promise.all([ - ks.removeKey('../../nasty').then(fail, err => err), - ks.removeKey('').then(fail, err => err), - ks.removeKey(' ').then(fail, err => err), - ks.removeKey(null).then(fail, err => err), - ks.removeKey(undefined).then(fail, err => err) - ]) - - expect(errors).to.have.length(5) - errors.forEach(error => { - expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - }) - }) - - describe('key', () => { - it('can be an RSA key', async () => { - rsaKeyInfo = await ks.createKey(rsaKeyName, 'rsa', 2048) - expect(rsaKeyInfo).to.exist() - expect(rsaKeyInfo).to.have.property('name', rsaKeyName) - expect(rsaKeyInfo).to.have.property('id') - }) - - it('is encrypted PEM encoded PKCS #8', async () => { - const pem = await ks._getPrivateKey(rsaKeyName) - return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - }) +const { expect } = chai - it('throws if an invalid private key name is given', async () => { - const err = await ks._getPrivateKey(undefined).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('throws if a private key cant be found', async () => { - const err = await ks._getPrivateKey('not real').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') - }) +const peerUtils = require('../utils/creators/peer') - it('does not overwrite existing key', async () => { - const err = await ks.createKey(rsaKeyName, 'rsa', 2048).then(fail, err => err) - expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') +describe('libp2p.keychain', () => { + it('needs a passphrase to be used, otherwise throws an error', async () => { + const [libp2p] = await peerUtils.createPeer({ + started: false }) - it('cannot create the "self" key', async () => { - const err = await ks.createKey('self', 'rsa', 2048).then(fail, err => err) + try { + await libp2p.keychain.createKey('keyName', 'rsa', 2048) + } catch (err) { expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('should validate name is string', async () => { - const err = await ks.createKey(5, 'rsa', 2048).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('should validate type is string', async () => { - const err = await ks.createKey('TEST' + Date.now(), null, 2048).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_TYPE') - }) - - it('should validate size is integer', async () => { - const err = await ks.createKey('TEST' + Date.now(), 'rsa', 'string').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') - }) - - describe('implements NIST SP 800-131A', () => { - it('disallows RSA length < 2048', async () => { - const err = await ks.createKey('bad-nist-rsa', 'rsa', 1024).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') - }) - }) - }) - - describe('query', () => { - it('finds all existing keys', async () => { - const keys = await ks.listKeys() - expect(keys).to.exist() - const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) - expect(mykey).to.exist() - }) - - it('finds a key by name', async () => { - const key = await ks.findKeyByName(rsaKeyName) - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - }) - - it('finds a key by id', async () => { - const key = await ks.findKeyById(rsaKeyInfo.id) - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - }) - - it('returns the key\'s name and id', async () => { - const keys = await ks.listKeys() - expect(keys).to.exist() - keys.forEach((key) => { - expect(key).to.have.property('name') - expect(key).to.have.property('id') - }) - }) + return + } + throw new Error('should throw an error using the keychain if no passphrase provided') }) - describe('CMS protected data', () => { - const plainData = Buffer.from('This is a message from Alice to Bob') - let cms - - it('service is available', () => { - expect(ks).to.have.property('cms') - }) - - it('requires a key', async () => { - const err = await ks.cms.encrypt('no-key', plainData).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') - }) - - it('requires plain data as a Buffer', async () => { - const err = await ks.cms.encrypt(rsaKeyName, 'plain data').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_PARAMS') - }) - - it('encrypts', async () => { - cms = await ks.cms.encrypt(rsaKeyName, plainData) - expect(cms).to.exist() - expect(cms).to.be.instanceOf(Buffer) - }) - - it('is a PKCS #7 message', async () => { - const err = await ks.cms.decrypt('not CMS').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_PARAMS') - }) - - it('is a PKCS #7 binary message', async () => { - const err = await ks.cms.decrypt(plainData).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_CMS') - }) - - it('cannot be read without the key', async () => { - const err = await emptyKeystore.cms.decrypt(cms).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('missingKeys') - expect(err.missingKeys).to.eql([rsaKeyInfo.id]) - expect(err).to.have.property('code', 'ERR_MISSING_KEYS') + it('can be used if a passphrase is provided', async () => { + const [libp2p] = await peerUtils.createPeer({ + started: false, + config: { + keychain: { + pass: '12345678901234567890' + } + } }) - it('can be read with the key', async () => { - const plain = await ks.cms.decrypt(cms) - expect(plain).to.exist() - expect(plain.toString()).to.equal(plainData.toString()) - }) - }) - - describe('exported key', () => { - let pemKey - - it('requires the password', async () => { - const err = await ks.exportKey(rsaKeyName).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_PASSWORD_REQUIRED') - }) - - it('requires the key name', async () => { - const err = await ks.exportKey(undefined, 'password').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('is a PKCS #8 encrypted pem', async () => { - pemKey = await ks.exportKey(rsaKeyName, 'password') - expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - }) - - it('can be imported', async () => { - const key = await ks.importKey('imported-key', pemKey, 'password') - expect(key.name).to.equal('imported-key') - expect(key.id).to.equal(rsaKeyInfo.id) - }) - - it('requires the pem', async () => { - const err = await ks.importKey('imported-key', undefined, 'password').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_PEM_REQUIRED') - }) - - it('cannot be imported as an existing key name', async () => { - const err = await ks.importKey(rsaKeyName, pemKey, 'password').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('cannot be imported with the wrong password', async () => { - const err = await ks.importKey('a-new-name-for-import', pemKey, 'not the password').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_CANNOT_READ_KEY') - }) - }) - - describe('peer id', () => { - const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' - let alice - - before(async function () { - const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await PeerId.createFromPrivKey(encoded) - }) - - it('private key can be imported', async () => { - const key = await ks.importPeer('alice', alice) - expect(key.name).to.equal('alice') - expect(key.id).to.equal(alice.toB58String()) - }) - - it('private key import requires a valid name', async () => { - const err = await ks.importPeer(undefined, alice).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('private key import requires the peer', async () => { - const err = await ks.importPeer('alice').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_MISSING_PRIVATE_KEY') - }) - - it('key id exists', async () => { - const key = await ks.findKeyById(alice.toB58String()) - expect(key).to.exist() - expect(key).to.have.property('name', 'alice') - expect(key).to.have.property('id', alice.toB58String()) - }) - - it('key name exists', async () => { - const key = await ks.findKeyByName('alice') - expect(key).to.exist() - expect(key).to.have.property('name', 'alice') - expect(key).to.have.property('id', alice.toB58String()) - }) - }) - - describe('rename', () => { - it('requires an existing key name', async () => { - const err = await ks.renameKey('not-there', renamedRsaKeyName).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_NOT_FOUND') - }) - - it('requires a valid new key name', async () => { - const err = await ks.renameKey(rsaKeyName, '..\not-valid').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') - }) - - it('does not overwrite existing key', async () => { - const err = await ks.renameKey(rsaKeyName, rsaKeyName).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('cannot create the "self" key', async () => { - const err = await ks.renameKey(rsaKeyName, 'self').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') - }) - - it('removes the existing key name', async () => { - const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName) - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - // Try to find the changed key - const err = await ks.findKeyByName(rsaKeyName).then(fail, err => err) - expect(err).to.exist() - }) - - it('creates the new key name', async () => { - const key = await ks.findKeyByName(renamedRsaKeyName) - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - }) - - it('does not change the key ID', async () => { - const key = await ks.findKeyByName(renamedRsaKeyName) - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - }) - - it('throws with invalid key names', async () => { - const err = await ks.findKeyByName(undefined).then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - }) - - describe('key removal', () => { - it('cannot remove the "self" key', async () => { - const err = await ks.removeKey('self').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('cannot remove an unknown key', async () => { - const err = await ks.removeKey('not-there').then(fail, err => err) - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') - }) - - it('can remove a known key', async () => { - const key = await ks.removeKey(renamedRsaKeyName) - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - }) + const kInfo = await libp2p.keychain.createKey('keyName', 'rsa', 2048) + expect(kInfo).to.exist() }) })