From 6065923356064795d03fed3cbe474d31e91a9f3d Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 12 May 2020 14:09:31 +0200 Subject: [PATCH] chore: integrate libp2p-keychain into js-libp2p (#633) Integrates the libp2p-keychain codebase into this repo --- .aegir.js | 2 +- doc/API.md | 319 +++++++++ doc/CONFIGURATION.md | 32 + src/config.js | 4 + src/index.js | 31 + src/keychain/README.md | 68 -- src/keychain/index.js | 468 +++++++++++- src/keychain/keychain.js | 469 ------------ test/keychain/browser.js | 27 - .../{cms-interop.js => cms-interop.spec.js} | 56 +- test/keychain/keychain.spec.js | 672 ++++++++++-------- test/keychain/node.js | 31 - test/keychain/{peerid.js => peerid.spec.js} | 0 13 files changed, 1261 insertions(+), 918 deletions(-) delete mode 100644 src/keychain/keychain.js delete mode 100644 test/keychain/browser.js rename test/keychain/{cms-interop.js => cms-interop.spec.js} (59%) delete mode 100644 test/keychain/node.js rename test/keychain/{peerid.js => peerid.spec.js} (100%) 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 883c67f5d5..3ac090db1d 100644 --- a/doc/API.md +++ b/doc/API.md @@ -44,6 +44,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.listKeys`](#keychainlistkeys) + * [`keychain.findKeyById`](#keychainfindkeybyid) + * [`keychain.findKeyByName`](#keychainfindkeybyname) + * [`keychain.cms.encrypt`](#keychaincmsencrypt) + * [`keychain.cms.decrypt`](#keychaincmsdecrypt) * [`metrics.global`](#metricsglobal) * [`metrics.peers`](#metricspeers) * [`metrics.protocols`](#metricsprotocols) @@ -75,6 +86,7 @@ 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.keychain] | [`object`](./CONFIGURATION.md#setup-with-keychain) | 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 | @@ -125,6 +137,36 @@ Required keys in the `options` object: ## Libp2p Instance Methods +### loadKeychain + +Load keychain keys from the datastore, importing the private key as 'self', if needed. + +`libp2p.loadKeychain()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves when the keychain is ready | + +#### Example + +```js +const Libp2p = require('libp2p') + +// ... + +const libp2p = await Libp2p.create({ + // ... + keychain: { + pass: '0123456789pass1234567890' + } +}) + +// load keychain +await libp2p.loadKeychain() +``` + ### start Starts the libp2p node. @@ -1254,6 +1296,283 @@ 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.listKeys + +List all the keys. + +`libp2p.keychain.listKeys()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise>` | Array of Key info | + +#### Example + +```js +const keyInfos = await libp2p.keychain.listKeys() +``` + +### keychain.findKeyById + +Find a key by it's id. + +`libp2p.keychain.findKeyById(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.findKeyById(keyInfo.id) +``` + +### keychain.findKeyByName + +Find a key by it's name. + +`libp2p.keychain.findKeyByName(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.findKeyByName('keyTest') +``` + +### keychain.cms.encrypt + +Encrypt protected data using the Cryptographic Message Syntax (CMS). + +`libp2p.keychain.cms.encrypt(name, data)` + +#### Parameters + +| 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)` + +#### Parameters + +| 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')) +const decData = await libp2p.keychain.cms.decrypt(enc) +``` + ### 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 b8d038943e..9dcc141ca9 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,37 @@ 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') + } +}) + +await libp2p.loadKeychain() +``` + #### 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/src/config.js b/src/config.js index 53c9c4c2b5..2bd5a189fe 100644 --- a/src/config.js +++ b/src/config.js @@ -1,5 +1,6 @@ 'use strict' +const { MemoryDatastore } = require('interface-datastore') const mergeOptions = require('merge-options') const Constants = require('./constants') @@ -17,6 +18,9 @@ const DefaultConfig = { maxDialsPerPeer: Constants.MAX_PER_PEER_DIALS, dialTimeout: Constants.DIAL_TIMEOUT }, + keychain: { + datastore: new MemoryDatastore() + }, metrics: { enabled: false }, diff --git a/src/index.js b/src/index.js index c839d40530..28954ae5cb 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ const AddressManager = require('./address-manager') const ConnectionManager = require('./connection-manager') const Circuit = require('./circuit') const Dialer = require('./dialer') +const Keychain = require('./keychain') const Metrics = require('./metrics') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') @@ -74,6 +75,22 @@ class Libp2p extends EventEmitter { }) } + // Create keychain + if (this._options.keychain.pass) { + log('creating keychain') + + const datastore = this._options.keychain.datastore + const keychainOpts = Keychain.generateOptions() + + this.keychain = new Keychain(datastore, { + passPhrase: this._options.keychain.pass, + ...keychainOpts, + ...this._options.keychain + }) + + log('keychain constructed') + } + // Setup the Upgrader this.upgrader = new Upgrader({ localPeer: this.peerId, @@ -249,6 +266,20 @@ class Libp2p extends EventEmitter { log('libp2p has stopped') } + /** + * Load keychain keys from the datastore. + * Imports the private key as 'self', if needed. + * @async + * @returns {void} + */ + async loadKeychain () { + try { + await this.keychain.findKeyByName('self') + } catch (err) { + await this.keychain.importPeer('self', this.peerId) + } + } + isStarted () { return this._isStarted } 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/index.js b/src/keychain/index.js index 2704d6268a..9af46d2628 100644 --- a/src/keychain/index.js +++ b/src/keychain/index.js @@ -1,3 +1,469 @@ +/* eslint max-nested-callbacks: ["error", 5] */ 'use strict' -module.exports = require('./keychain') +const sanitize = require('sanitize-filename') +const mergeOptions = require('merge-options') +const crypto = require('libp2p-crypto') +const DS = require('interface-datastore') +const CMS = require('./cms') +const errcode = require('err-code') + +const keyPrefix = '/pkcs8/' +const infoPrefix = '/info/' + +// NIST SP 800-132 +const NIST = { + minKeyLength: 112 / 8, + minSaltLength: 128 / 8, + minIterationCount: 1000 +} + +const defaultOptions = { + // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + salt: 'you should override this value with a crypto secure random number', + hash: 'sha2-512' + } +} + +function validateKeyName (name) { + if (!name) return false + if (typeof name !== 'string') return false + return name === sanitize(name.trim()) +} + +/** + * Throws an error after a delay + * + * This assumes than an error indicates that the keychain is under attack. Delay returning an + * error to make brute force attacks harder. + * + * @param {string | Error} err - The error + * @private + */ +async function throwDelayed (err) { + const min = 200 + const max = 1000 + const delay = Math.random() * (max - min) + min + + await new Promise(resolve => setTimeout(resolve, delay)) + throw err +} + +/** + * Converts a key name into a datastore name. + * + * @param {string} name + * @returns {DS.Key} + * @private + */ +function DsName (name) { + return new DS.Key(keyPrefix + name) +} + +/** + * Converts a key name into a datastore info name. + * + * @param {string} name + * @returns {DS.Key} + * @private + */ +function DsInfoName (name) { + return new DS.Key(infoPrefix + name) +} + +/** + * Information about a key. + * + * @typedef {Object} KeyInfo + * + * @property {string} id - The universally unique key id. + * @property {string} name - The local key name. + */ + +/** + * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. + * + * A key in the store has two entries + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key + * + */ +class Keychain { + /** + * Creates a new instance of a key chain. + * + * @param {DS} store - where the key are. + * @param {object} options - ??? + */ + constructor (store, options) { + if (!store) { + throw new Error('store is required') + } + this.store = store + + this.opts = mergeOptions(defaultOptions, options) + + // Enforce NIST SP 800-132 + if (!this.opts.passPhrase || this.opts.passPhrase.length < 20) { + throw new Error('passPhrase must be least 20 characters') + } + if (this.opts.dek.keyLength < NIST.minKeyLength) { + throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) + } + if (this.opts.dek.salt.length < NIST.minSaltLength) { + throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) + } + 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( + this.opts.passPhrase, + this.opts.dek.salt, + this.opts.dek.iterationCount, + this.opts.dek.keyLength, + this.opts.dek.hash) + Object.defineProperty(this, '_', { value: () => dek }) + } + + /** + * Gets an object that can encrypt/decrypt protected data + * using the Cryptographic Message Syntax (CMS). + * + * CMS describes an encapsulation syntax for data protection. It + * is used to digitally sign, digest, authenticate, or encrypt + * arbitrary message content. + * + * @returns {CMS} + */ + get cms () { + return new CMS(this) + } + + /** + * Generates the options for a keychain. A random salt is produced. + * + * @returns {object} + */ + static generateOptions () { + const options = Object.assign({}, defaultOptions) + const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding + options.dek.salt = crypto.randomBytes(saltLength).toString('base64') + return options + } + + /** + * Gets an object that can encrypt/decrypt protected data. + * The default options for a keychain. + * + * @returns {object} + */ + static get options () { + return defaultOptions + } + + /** + * Create a new key. + * + * @param {string} name - The local key name; cannot already exist. + * @param {string} type - One of the key types; 'rsa'. + * @param {int} size - The key size in bits. + * @returns {KeyInfo} + */ + async createKey (name, type, size) { + const self = this + + if (!validateKeyName(name) || name === 'self') { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + + if (typeof type !== 'string') { + return throwDelayed(errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE')) + } + + if (!Number.isSafeInteger(size)) { + return throwDelayed(errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE')) + } + + const dsname = DsName(name) + const exists = await self.store.has(dsname) + if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) + + switch (type.toLowerCase()) { + case 'rsa': + if (size < 2048) { + return throwDelayed(errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE')) + } + break + default: + break + } + + let keyInfo + try { + const keypair = await crypto.keys.generateKeyPair(type, size) + const kid = await keypair.id() + const pem = await keypair.export(this._()) + keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, pem) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + + await batch.commit() + } catch (err) { + return throwDelayed(err) + } + + return keyInfo + } + + /** + * List all the keys. + * + * @returns {KeyInfo[]} + */ + async listKeys () { + const self = this + const query = { + prefix: infoPrefix + } + + const info = [] + for await (const value of self.store.query(query)) { + info.push(JSON.parse(value.value)) + } + + return info + } + + /** + * Find a key by it's id. + * + * @param {string} id - The universally unique key identifier. + * @returns {KeyInfo} + */ + async findKeyById (id) { + try { + const keys = await this.listKeys() + return keys.find((k) => k.id === id) + } catch (err) { + return throwDelayed(err) + } + } + + /** + * Find a key by it's name. + * + * @param {string} name - The local key name. + * @returns {KeyInfo} + */ + async findKeyByName (name) { + if (!validateKeyName(name)) { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + + const dsname = DsInfoName(name) + try { + const res = await this.store.get(dsname) + return JSON.parse(res.toString()) + } catch (err) { + return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) + } + } + + /** + * Remove an existing key. + * + * @param {string} name - The local key name; must already exist. + * @returns {KeyInfo} + */ + async removeKey (name) { + const self = this + if (!validateKeyName(name) || name === 'self') { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + const dsname = DsName(name) + const keyInfo = await self.findKeyByName(name) + const batch = self.store.batch() + batch.delete(dsname) + batch.delete(DsInfoName(name)) + await batch.commit() + return keyInfo + } + + /** + * Rename a key + * + * @param {string} oldName - The old local key name; must already exist. + * @param {string} newName - The new local key name; must not already exist. + * @returns {KeyInfo} + */ + async renameKey (oldName, newName) { + const self = this + if (!validateKeyName(oldName) || oldName === 'self') { + return throwDelayed(errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID')) + } + if (!validateKeyName(newName) || newName === 'self') { + return throwDelayed(errcode(new Error(`Invalid new key name '${newName}'`), 'ERR_NEW_KEY_NAME_INVALID')) + } + const oldDsname = DsName(oldName) + const newDsname = DsName(newName) + const oldInfoName = DsInfoName(oldName) + const newInfoName = DsInfoName(newName) + + const exists = await self.store.has(newDsname) + if (exists) return throwDelayed(errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) + + try { + let res = await this.store.get(oldDsname) + const pem = res.toString() + res = await self.store.get(oldInfoName) + + const keyInfo = JSON.parse(res.toString()) + keyInfo.name = newName + const batch = self.store.batch() + batch.put(newDsname, pem) + batch.put(newInfoName, JSON.stringify(keyInfo)) + batch.delete(oldDsname) + batch.delete(oldInfoName) + await batch.commit() + return keyInfo + } catch (err) { + return throwDelayed(err) + } + } + + /** + * Export an existing key as a PEM encrypted PKCS #8 string + * + * @param {string} name - The local key name; must already exist. + * @param {string} password - The password + * @returns {string} + */ + async exportKey (name, password) { + if (!validateKeyName(name)) { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + if (!password) { + return throwDelayed(errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED')) + } + + const dsname = DsName(name) + try { + const res = await this.store.get(dsname) + const pem = res.toString() + const privateKey = await crypto.keys.import(pem, this._()) + return privateKey.export(password) + } catch (err) { + return throwDelayed(err) + } + } + + /** + * Import a new key from a PEM encoded PKCS #8 string + * + * @param {string} name - The local key name; must not already exist. + * @param {string} pem - The PEM encoded PKCS #8 string + * @param {string} password - The password. + * @returns {KeyInfo} + */ + async importKey (name, pem, password) { + const self = this + if (!validateKeyName(name) || name === 'self') { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + if (!pem) { + return throwDelayed(errcode(new Error('PEM encoded key is required'), 'ERR_PEM_REQUIRED')) + } + const dsname = DsName(name) + const exists = await self.store.has(dsname) + if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) + + let privateKey + try { + privateKey = await crypto.keys.import(pem, password) + } catch (err) { + return throwDelayed(errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY')) + } + + let kid + try { + kid = await privateKey.id() + pem = await privateKey.export(this._()) + } catch (err) { + return throwDelayed(err) + } + + const keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, pem) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + await batch.commit() + + return keyInfo + } + + async importPeer (name, peer) { + const self = this + if (!validateKeyName(name)) { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + if (!peer || !peer.privKey) { + return throwDelayed(errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY')) + } + + const privateKey = peer.privKey + const dsname = DsName(name) + const exists = await self.store.has(dsname) + if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) + + try { + const kid = await privateKey.id() + const pem = await privateKey.export(this._()) + const keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, pem) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + await batch.commit() + return keyInfo + } catch (err) { + return throwDelayed(err) + } + } + + /** + * Gets the private key as PEM encoded PKCS #8 string. + * + * @param {string} name + * @returns {string} + * @private + */ + async _getPrivateKey (name) { + if (!validateKeyName(name)) { + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + } + + try { + const dsname = DsName(name) + const res = await this.store.get(dsname) + return res.toString() + } catch (err) { + return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) + } + } +} + +module.exports = Keychain diff --git a/src/keychain/keychain.js b/src/keychain/keychain.js deleted file mode 100644 index aae7897224..0000000000 --- a/src/keychain/keychain.js +++ /dev/null @@ -1,469 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const sanitize = require('sanitize-filename') -const mergeOptions = require('merge-options') -const crypto = require('libp2p-crypto') -const DS = require('interface-datastore') -const CMS = require('./cms') -const errcode = require('err-code') - -const keyPrefix = '/pkcs8/' -const infoPrefix = '/info/' - -// NIST SP 800-132 -const NIST = { - minKeyLength: 112 / 8, - minSaltLength: 128 / 8, - minIterationCount: 1000 -} - -const defaultOptions = { - // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ - dek: { - keyLength: 512 / 8, - iterationCount: 10000, - salt: 'you should override this value with a crypto secure random number', - hash: 'sha2-512' - } -} - -function validateKeyName (name) { - if (!name) return false - if (typeof name !== 'string') return false - return name === sanitize(name.trim()) -} - -/** - * Throws an error after a delay - * - * This assumes than an error indicates that the keychain is under attack. Delay returning an - * error to make brute force attacks harder. - * - * @param {string | Error} err - The error - * @private - */ -async function throwDelayed (err) { - const min = 200 - const max = 1000 - const delay = Math.random() * (max - min) + min - - await new Promise(resolve => setTimeout(resolve, delay)) - throw err -} - -/** - * Converts a key name into a datastore name. - * - * @param {string} name - * @returns {DS.Key} - * @private - */ -function DsName (name) { - return new DS.Key(keyPrefix + name) -} - -/** - * Converts a key name into a datastore info name. - * - * @param {string} name - * @returns {DS.Key} - * @private - */ -function DsInfoName (name) { - return new DS.Key(infoPrefix + name) -} - -/** - * Information about a key. - * - * @typedef {Object} KeyInfo - * - * @property {string} id - The universally unique key id. - * @property {string} name - The local key name. - */ - -/** - * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. - * - * A key in the store has two entries - * - '/info/*key-name*', contains the KeyInfo for the key - * - '/pkcs8/*key-name*', contains the PKCS #8 for the key - * - */ -class Keychain { - /** - * Creates a new instance of a key chain. - * - * @param {DS} store - where the key are. - * @param {object} options - ??? - */ - constructor (store, options) { - if (!store) { - throw new Error('store is required') - } - this.store = store - - const opts = mergeOptions(defaultOptions, options) - - // Enforce NIST SP 800-132 - if (!opts.passPhrase || opts.passPhrase.length < 20) { - throw new Error('passPhrase must be least 20 characters') - } - if (opts.dek.keyLength < NIST.minKeyLength) { - throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) - } - if (opts.dek.salt.length < NIST.minSaltLength) { - throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) - } - if (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) - Object.defineProperty(this, '_', { value: () => dek }) - } - - /** - * Gets an object that can encrypt/decrypt protected data - * using the Cryptographic Message Syntax (CMS). - * - * CMS describes an encapsulation syntax for data protection. It - * is used to digitally sign, digest, authenticate, or encrypt - * arbitrary message content. - * - * @returns {CMS} - */ - get cms () { - return new CMS(this) - } - - /** - * Generates the options for a keychain. A random salt is produced. - * - * @returns {object} - */ - static generateOptions () { - const options = Object.assign({}, defaultOptions) - const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding - options.dek.salt = crypto.randomBytes(saltLength).toString('base64') - return options - } - - /** - * Gets an object that can encrypt/decrypt protected data. - * The default options for a keychain. - * - * @returns {object} - */ - static get options () { - return defaultOptions - } - - /** - * Create a new key. - * - * @param {string} name - The local key name; cannot already exist. - * @param {string} type - One of the key types; 'rsa'. - * @param {int} size - The key size in bits. - * @returns {KeyInfo} - */ - async createKey (name, type, size) { - const self = this - - if (!validateKeyName(name) || name === 'self') { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - - if (typeof type !== 'string') { - return throwDelayed(errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE')) - } - - if (!Number.isSafeInteger(size)) { - return throwDelayed(errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE')) - } - - const dsname = DsName(name) - const exists = await self.store.has(dsname) - if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - switch (type.toLowerCase()) { - case 'rsa': - if (size < 2048) { - return throwDelayed(errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE')) - } - break - default: - break - } - - let keyInfo - try { - const keypair = await crypto.keys.generateKeyPair(type, size) - const kid = await keypair.id() - const pem = await keypair.export(this._()) - keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - - await batch.commit() - } catch (err) { - return throwDelayed(err) - } - - return keyInfo - } - - /** - * List all the keys. - * - * @returns {KeyInfo[]} - */ - async listKeys () { - const self = this - const query = { - prefix: infoPrefix - } - - const info = [] - for await (const value of self.store.query(query)) { - info.push(JSON.parse(value.value)) - } - - return info - } - - /** - * Find a key by it's id. - * - * @param {string} id - The universally unique key identifier. - * @returns {KeyInfo} - */ - async findKeyById (id) { - try { - const keys = await this.listKeys() - return keys.find((k) => k.id === id) - } catch (err) { - return throwDelayed(err) - } - } - - /** - * Find a key by it's name. - * - * @param {string} name - The local key name. - * @returns {KeyInfo} - */ - async findKeyByName (name) { - if (!validateKeyName(name)) { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - - const dsname = DsInfoName(name) - try { - const res = await this.store.get(dsname) - return JSON.parse(res.toString()) - } catch (err) { - return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } - } - - /** - * Remove an existing key. - * - * @param {string} name - The local key name; must already exist. - * @returns {KeyInfo} - */ - async removeKey (name) { - const self = this - if (!validateKeyName(name) || name === 'self') { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - const dsname = DsName(name) - const keyInfo = await self.findKeyByName(name) - const batch = self.store.batch() - batch.delete(dsname) - batch.delete(DsInfoName(name)) - await batch.commit() - return keyInfo - } - - /** - * Rename a key - * - * @param {string} oldName - The old local key name; must already exist. - * @param {string} newName - The new local key name; must not already exist. - * @returns {KeyInfo} - */ - async renameKey (oldName, newName) { - const self = this - if (!validateKeyName(oldName) || oldName === 'self') { - return throwDelayed(errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID')) - } - if (!validateKeyName(newName) || newName === 'self') { - return throwDelayed(errcode(new Error(`Invalid new key name '${newName}'`), 'ERR_NEW_KEY_NAME_INVALID')) - } - const oldDsname = DsName(oldName) - const newDsname = DsName(newName) - const oldInfoName = DsInfoName(oldName) - const newInfoName = DsInfoName(newName) - - const exists = await self.store.has(newDsname) - if (exists) return throwDelayed(errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - try { - let res = await this.store.get(oldDsname) - const pem = res.toString() - res = await self.store.get(oldInfoName) - - const keyInfo = JSON.parse(res.toString()) - keyInfo.name = newName - const batch = self.store.batch() - batch.put(newDsname, pem) - batch.put(newInfoName, JSON.stringify(keyInfo)) - batch.delete(oldDsname) - batch.delete(oldInfoName) - await batch.commit() - return keyInfo - } catch (err) { - return throwDelayed(err) - } - } - - /** - * Export an existing key as a PEM encrypted PKCS #8 string - * - * @param {string} name - The local key name; must already exist. - * @param {string} password - The password - * @returns {string} - */ - async exportKey (name, password) { - if (!validateKeyName(name)) { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - if (!password) { - return throwDelayed(errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED')) - } - - const dsname = DsName(name) - try { - const res = await this.store.get(dsname) - const pem = res.toString() - const privateKey = await crypto.keys.import(pem, this._()) - return privateKey.export(password) - } catch (err) { - return throwDelayed(err) - } - } - - /** - * Import a new key from a PEM encoded PKCS #8 string - * - * @param {string} name - The local key name; must not already exist. - * @param {string} pem - The PEM encoded PKCS #8 string - * @param {string} password - The password. - * @returns {KeyInfo} - */ - async importKey (name, pem, password) { - const self = this - if (!validateKeyName(name) || name === 'self') { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - if (!pem) { - return throwDelayed(errcode(new Error('PEM encoded key is required'), 'ERR_PEM_REQUIRED')) - } - const dsname = DsName(name) - const exists = await self.store.has(dsname) - if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - let privateKey - try { - privateKey = await crypto.keys.import(pem, password) - } catch (err) { - return throwDelayed(errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY')) - } - - let kid - try { - kid = await privateKey.id() - pem = await privateKey.export(this._()) - } catch (err) { - return throwDelayed(err) - } - - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - await batch.commit() - - return keyInfo - } - - async importPeer (name, peer) { - const self = this - if (!validateKeyName(name)) { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - if (!peer || !peer.privKey) { - return throwDelayed(errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY')) - } - - const privateKey = peer.privKey - const dsname = DsName(name) - const exists = await self.store.has(dsname) - if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - try { - const kid = await privateKey.id() - const pem = await privateKey.export(this._()) - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - await batch.commit() - return keyInfo - } catch (err) { - return throwDelayed(err) - } - } - - /** - * Gets the private key as PEM encoded PKCS #8 string. - * - * @param {string} name - * @returns {string} - * @private - */ - async _getPrivateKey (name) { - if (!validateKeyName(name)) { - return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) - } - - try { - const dsname = DsName(name) - const res = await this.store.get(dsname) - return res.toString() - } catch (err) { - return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } - } -} - -module.exports = Keychain diff --git a/test/keychain/browser.js b/test/keychain/browser.js deleted file mode 100644 index 02222fb3b3..0000000000 --- a/test/keychain/browser.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const LevelStore = require('datastore-level') - -describe('browser', () => { - const datastore1 = new LevelStore('test-keystore-1', { db: require('level') }) - const datastore2 = new LevelStore('test-keystore-2', { db: require('level') }) - - before(() => { - return Promise.all([ - datastore1.open(), - datastore2.open() - ]) - }) - - after(() => { - return Promise.all([ - datastore1.close(), - datastore2.close() - ]) - }) - - require('./keychain.spec')(datastore1, datastore2) - require('./cms-interop')(datastore2) - require('./peerid') -}) diff --git a/test/keychain/cms-interop.js b/test/keychain/cms-interop.spec.js similarity index 59% rename from test/keychain/cms-interop.js rename to test/keychain/cms-interop.spec.js index e32764c9c6..8eb19f6e99 100644 --- a/test/keychain/cms-interop.js +++ b/test/keychain/cms-interop.spec.js @@ -7,24 +7,33 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) chai.use(require('chai-string')) + +const os = require('os') +const path = require('path') +const { isNode } = require('ipfs-utils/src/env') +const FsStore = require('datastore-fs') +const LevelStore = require('datastore-level') + const Keychain = require('../../src/keychain') -module.exports = (datastore) => { - describe('cms interop', () => { - const passPhrase = 'this is not a secure phrase' - const aliceKeyName = 'cms-interop-alice' - let ks +describe('cms interop', () => { + const passPhrase = 'this is not a secure phrase' + const aliceKeyName = 'cms-interop-alice' + let ks - before(() => { - ks = new Keychain(datastore, { passPhrase: passPhrase }) - }) + before(() => { + const datastore = isNode + ? new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())) + : new LevelStore('test-keystore-1', { db: require('level') }) + ks = new Keychain(datastore, { passPhrase: passPhrase }) + }) - const plainData = Buffer.from('This is a message from Alice to Bob') + const plainData = Buffer.from('This is a message from Alice to Bob') - it('imports openssl key', async function () { - this.timeout(10 * 1000) - const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA' - const alice = `-----BEGIN ENCRYPTED PRIVATE KEY----- + it('imports openssl key', async function () { + this.timeout(10 * 1000) + const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA' + const alice = `-----BEGIN ENCRYPTED PRIVATE KEY----- MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd @@ -42,13 +51,13 @@ igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL -----END ENCRYPTED PRIVATE KEY----- ` - const key = await ks.importKey(aliceKeyName, alice, 'mypassword') - expect(key.name).to.equal(aliceKeyName) - expect(key.id).to.equal(aliceKid) - }) + const key = await ks.importKey(aliceKeyName, alice, 'mypassword') + expect(key.name).to.equal(aliceKeyName) + expect(key.id).to.equal(aliceKid) + }) - it('decrypts node-forge example', async () => { - const example = ` + it('decrypts node-forge example', async () => { + const example = ` MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B @@ -58,9 +67,8 @@ knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3 DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N ` - const plain = await ks.cms.decrypt(Buffer.from(example, 'base64')) - expect(plain).to.exist() - expect(plain.toString()).to.equal(plainData.toString()) - }) + const plain = await ks.cms.decrypt(Buffer.from(example, 'base64')) + expect(plain).to.exist() + expect(plain.toString()).to.equal(plainData.toString()) }) -} +}) diff --git a/test/keychain/keychain.spec.js b/test/keychain/keychain.spec.js index c39a7f4868..2aad3c1cf8 100644 --- a/test/keychain/keychain.spec.js +++ b/test/keychain/keychain.spec.js @@ -3,381 +3,459 @@ 'use strict' const chai = require('chai') -const expect = chai.expect +const { expect } = chai const fail = expect.fail chai.use(require('dirty-chai')) chai.use(require('chai-string')) + +const peerUtils = require('../utils/creators/peer') + +const os = require('os') +const path = require('path') +const { isNode } = require('ipfs-utils/src/env') +const { MemoryDatastore } = require('interface-datastore') +const FsStore = require('datastore-fs') +const LevelStore = require('datastore-level') const Keychain = require('../../src/keychain') const PeerId = require('peer-id') -module.exports = (datastore1, datastore2) => { - describe('keychain', () => { - const passPhrase = 'this is not a secure phrase' - const rsaKeyName = 'tajné jméno' - const renamedRsaKeyName = 'ชื่อลับ' - let rsaKeyInfo - let emptyKeystore - let ks +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 = isNode + ? new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())) + : new LevelStore('test-keystore-1', { db: require('level') }) + datastore2 = isNode + ? new FsStore(path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())) + : new LevelStore('test-keystore-2', { db: require('level') }) + + ks = new Keychain(datastore2, { passPhrase: passPhrase }) + emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) + }) - before((done) => { - ks = new Keychain(datastore2, { passPhrase: passPhrase }) - emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) - done() - }) + it('needs a pass phrase to encrypt a key', () => { + expect(() => new Keychain(datastore2)).to.throw() + }) - 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('needs a NIST SP 800-132 non-weak pass phrase', () => { - expect(() => new Keychain(datastore2, { passPhrase: '< 20 character' })).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') + }) }) + }) - it('needs a store to persist a key', () => { - expect(() => new Keychain(null, { passPhrase: passPhrase })).to.throw() + 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('has default options', () => { - expect(Keychain.options).to.exist() + it('is encrypted PEM encoded PKCS #8', async () => { + const pem = await ks._getPrivateKey(rsaKeyName) + return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') }) - 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('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('can generate options', () => { - const options = Keychain.generateOptions() - options.passPhrase = passPhrase - const chain = new Keychain(datastore2, options) - expect(chain).to.exist() + 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') }) - 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) - ]) + 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') + }) - expect(errors).to.have.length(5) - errors.forEach(error => { - expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - }) + 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') }) - 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('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('is encrypted PEM encoded PKCS #8', async () => { - const pem = await ks._getPrivateKey(rsaKeyName) - return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - }) + 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('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('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') + }) - it('throws if a private key cant be found', async () => { - const err = await ks._getPrivateKey('not real').then(fail, err => err) + 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_KEY_NOT_FOUND') + expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') }) + }) + }) - 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('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('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('finds a key by name', async () => { + const key = await ks.findKeyByName(rsaKeyName) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) - 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('finds a key by id', async () => { + const key = await ks.findKeyById(rsaKeyInfo.id) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) - 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('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') }) + }) + }) - 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('CMS protected data', () => { + const plainData = Buffer.from('This is a message from Alice to Bob') + let cms - 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') - }) - }) + it('service is available', () => { + expect(ks).to.have.property('cms') }) - 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('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('finds a key by name', async () => { - const key = await ks.findKeyByName(rsaKeyName) - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - }) + 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('finds a key by id', async () => { - const key = await ks.findKeyById(rsaKeyInfo.id) - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - }) + it('encrypts', async () => { + cms = await ks.cms.encrypt(rsaKeyName, plainData) + expect(cms).to.exist() + expect(cms).to.be.instanceOf(Buffer) + }) - 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') - }) - }) + 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') }) - describe('CMS protected data', () => { - const plainData = Buffer.from('This is a message from Alice to Bob') - let cms + 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('service is available', () => { - expect(ks).to.have.property('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('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('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()) + }) + }) - 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') - }) + describe('exported key', () => { + let pemKey - it('encrypts', async () => { - cms = await ks.cms.encrypt(rsaKeyName, plainData) - expect(cms).to.exist() - expect(cms).to.be.instanceOf(Buffer) - }) + 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('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('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 #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('is a PKCS #8 encrypted pem', async () => { + pemKey = await ks.exportKey(rsaKeyName, 'password') + expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) - 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 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('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()) - }) + 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') }) - describe('exported key', () => { - let pemKey + 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('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('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') + }) + }) - 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') - }) + 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 - it('is a PKCS #8 encrypted pem', async () => { - pemKey = await ks.exportKey(rsaKeyName, 'password') - expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - }) + before(async function () { + const encoded = Buffer.from(alicePrivKey, 'base64') + alice = await PeerId.createFromPrivKey(encoded) + }) - 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('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('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('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('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('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('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') - }) + 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()) }) - 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 + 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()) + }) + }) - before(async function () { - const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await PeerId.createFromPrivKey(encoded) - }) + 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('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('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('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('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('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('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('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('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('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()) - }) + it('creates the new key name', async () => { + const key = await ks.findKeyByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) }) - 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('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('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('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') + }) + }) - 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') - }) + 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 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('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('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('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) + }) + }) +}) - it('creates the new key name', async () => { - const key = await ks.findKeyByName(renamedRsaKeyName) - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - }) +describe('libp2p.keychain', () => { + it('needs a passphrase to be used, otherwise throws an error', async () => { + const [libp2p] = await peerUtils.createPeer({ + started: false + }) - 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) - }) + try { + await libp2p.keychain.createKey('keyName', 'rsa', 2048) + } catch (err) { + expect(err).to.exist() + return + } + throw new Error('should throw an error using the keychain if no passphrase provided') + }) - 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') - }) + it('can be used if a passphrase is provided', async () => { + const [libp2p] = await peerUtils.createPeer({ + started: false, + config: { + keychain: { + pass: '12345678901234567890' + } + } }) - 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') - }) + await libp2p.loadKeychain() - 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') - }) + const kInfo = await libp2p.keychain.createKey('keyName', 'rsa', 2048) + expect(kInfo).to.exist() + }) - 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) - }) + it('can reload keys', async () => { + const datastore = new MemoryDatastore() + const [libp2p] = await peerUtils.createPeer({ + started: false, + config: { + keychain: { + datastore, + pass: '12345678901234567890' + } + } + }) + await libp2p.loadKeychain() + + const kInfo = await libp2p.keychain.createKey('keyName', 'rsa', 2048) + expect(kInfo).to.exist() + + const [libp2p2] = await peerUtils.createPeer({ + started: false, + config: { + keychain: { + datastore, + pass: '12345678901234567890' + } + } }) + + await libp2p2.loadKeychain() + const key = await libp2p2.keychain.findKeyByName('keyName') + + expect(key).to.exist() }) -} +}) diff --git a/test/keychain/node.js b/test/keychain/node.js deleted file mode 100644 index bbb2508960..0000000000 --- a/test/keychain/node.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const os = require('os') -const path = require('path') -const promisify = require('promisify-es6') -const rimraf = promisify(require('rimraf')) -const FsStore = require('datastore-fs') - -describe('node', () => { - const store1 = path.join(os.tmpdir(), 'test-keystore-1-' + Date.now()) - const store2 = path.join(os.tmpdir(), 'test-keystore-2-' + Date.now()) - const datastore1 = new FsStore(store1) - const datastore2 = new FsStore(store2) - - before(async () => { - await datastore1.open() - await datastore2.open() - }) - - after(async () => { - await datastore1.close() - await datastore2.close() - await rimraf(store1) - await rimraf(store2) - }) - - require('./keychain.spec')(datastore1, datastore2) - require('./cms-interop')(datastore2) - require('./peerid') -}) diff --git a/test/keychain/peerid.js b/test/keychain/peerid.spec.js similarity index 100% rename from test/keychain/peerid.js rename to test/keychain/peerid.spec.js