From dda315a9c8972a42b5bd7ef72afbb1a8f8f44f10 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 16 Aug 2019 13:12:47 +0200 Subject: [PATCH] refactor: use async/await instead of callbacks (#37) BREAKING CHANGE: The api now uses async/await instead of callbacks. Co-Authored-By: Vasco Santos --- .gitignore | 1 + .travis.yml | 8 +- README.md | 40 ++-- package.json | 34 ++- src/cms.js | 105 ++++----- src/keychain.js | 375 +++++++++++++++---------------- src/util.js | 24 +- test/browser.js | 25 +-- test/cms-interop.js | 25 +-- test/keychain.spec.js | 500 ++++++++++++++++++------------------------ test/node.js | 24 +- test/peerid.js | 72 ++---- 12 files changed, 537 insertions(+), 696 deletions(-) diff --git a/.gitignore b/.gitignore index b64f085..3da57e8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ logs *.log coverage +.nyc_output # Runtime data pids diff --git a/.travis.yml b/.travis.yml index 37005a2..2061bd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ stages: node_js: - '10' + - '12' os: - linux @@ -20,8 +21,7 @@ jobs: include: - stage: check script: - - npx aegir commitlint --travis - - npx aegir dep-check -- -i wrtc -i electron-webrtc + - npx aegir dep-check - npm run lint - stage: test @@ -29,14 +29,14 @@ jobs: addons: chrome: stable script: - - npx aegir test -t browser + - npx aegir test -t browser -t webworker - stage: test name: firefox addons: firefox: latest script: - - npx aegir test -t browser -- --browsers FirefoxHeadless + - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless notifications: email: false diff --git a/README.md b/README.md index 4ff4abe..37829b4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ # js-libp2p-keychain -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) -[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) -[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) +[![](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) -[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -[![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-keychain/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-keychain?branch=master) -[![Travis CI](https://travis-ci.org/libp2p/js-libp2p-keychain.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-keychain) -[![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-keychain.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-keychain) +[![](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) -![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) -![](https://img.shields.io/badge/Node.js-%3E%3D6.0.0-orange.svg?style=flat-square) > A secure key chain for libp2p in JavaScript @@ -55,23 +51,23 @@ const keychain = new Keychain(datastore, opts) Managing a key -- `createKey (name, type, size, callback)` -- `renameKey (oldName, newName, callback)` -- `removeKey (name, callback)` -- `exportKey (name, password, callback)` -- `importKey (name, pem, password, callback)` -- `importPeer (name, peer, callback)` +- `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 -- `listKeys (callback)` -- `findKeyById (id, callback)` -- `findKeyByName (name, callback)` +- `async listKeys ()` +- `async findKeyById (id)` +- `async findKeyByName (name)` Cryptographically protected messages -- `cms.encrypt (name, plain, callback)` -- `cms.decrypt (cmsData, callback)` +- `async cms.encrypt (name, plain)` +- `async cms.decrypt (cmsData)` ### KeyInfo @@ -116,11 +112,11 @@ CMS, aka [PKCS #7](https://en.wikipedia.org/wiki/PKCS) and [RFC 5652](https://to ## Contribute -Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)! +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) +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) ## License diff --git a/package.json b/package.json index b6526b3..eb3d308 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,19 @@ "scripts": { "lint": "aegir lint", "build": "aegir build", + "coverage": "nyc --reporter=text --reporter=lcov npm run test:node", "test": "aegir test -t node -t browser", "test:node": "aegir test -t node", "test:browser": "aegir test -t browser", "release": "aegir release", "release-minor": "aegir release --type minor", - "release-major": "aegir release --type major", - "coverage": "aegir coverage", - "coverage-publish": "aegir coverage publish" + "release-major": "aegir release --type major" }, "pre-push": [ - "lint", - "test" + "lint" ], "engines": { - "node": ">=6.0.0", + "node": ">=10.0.0", "npm": ">=3.0.0" }, "repository": { @@ -42,26 +40,24 @@ }, "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { - "async": "^2.6.2", - "err-code": "^1.1.2", - "interface-datastore": "~0.6.0", - "libp2p-crypto": "~0.16.1", + "err-code": "^2.0.0", + "interface-datastore": "^0.7.0", + "libp2p-crypto": "^0.17.0", "merge-options": "^1.0.1", - "node-forge": "~0.7.6", - "pull-stream": "^3.6.9", + "node-forge": "^0.8.5", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^18.2.1", + "aegir": "^20.0.0", "chai": "^4.2.0", "chai-string": "^1.5.0", - "datastore-fs": "~0.8.0", - "datastore-level": "~0.10.0", + "datastore-fs": "^0.9.0", + "datastore-level": "^0.12.1", "dirty-chai": "^2.0.1", - "level-js": "^4.0.1", - "mocha": "^5.2.0", - "multihashes": "~0.4.14", - "peer-id": "~0.12.2", + "level": "^5.0.1", + "multihashes": "^0.4.15", + "peer-id": "^0.13.2", + "promisify-es6": "^1.0.3", "rimraf": "^2.6.3" }, "contributors": [ diff --git a/src/cms.js b/src/cms.js index d086407..9bec4b9 100644 --- a/src/cms.js +++ b/src/cms.js @@ -1,13 +1,9 @@ 'use strict' -const setImmediate = require('async/setImmediate') -const series = require('async/series') -const detect = require('async/detect') -const waterfall = require('async/waterfall') require('node-forge/lib/pkcs7') require('node-forge/lib/pbe') const forge = require('node-forge/lib/forge') -const util = require('./util') +const { certificateForKey, findAsync } = require('./util') const errcode = require('err-code') /** @@ -40,44 +36,27 @@ class CMS { * * @param {string} name - The local key name. * @param {Buffer} plain - The data to encrypt. - * @param {function(Error, Buffer)} callback * @returns {undefined} */ - encrypt (name, plain, callback) { - const self = this - const done = (err, result) => setImmediate(() => callback(err, result)) - + async encrypt (name, plain) { if (!Buffer.isBuffer(plain)) { - return done(errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')) + throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS') } - series([ - (cb) => self.keychain.findKeyByName(name, cb), - (cb) => self.keychain._getPrivateKey(name, cb) - ], (err, results) => { - if (err) return done(err) - - let key = results[0] - let pem = results[1] - try { - const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._()) - util.certificateForKey(key, privateKey, (err, certificate) => { - if (err) return callback(err) + const key = await this.keychain.findKeyByName(name) + const pem = await this.keychain._getPrivateKey(name) + const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._()) + const certificate = await certificateForKey(key, privateKey) - // create a p7 enveloped message - const p7 = forge.pkcs7.createEnvelopedData() - p7.addRecipient(certificate) - p7.content = forge.util.createBuffer(plain) - p7.encrypt() + // create a p7 enveloped message + const p7 = forge.pkcs7.createEnvelopedData() + p7.addRecipient(certificate) + p7.content = forge.util.createBuffer(plain) + p7.encrypt() - // convert message to DER - const der = forge.asn1.toDer(p7.toAsn1()).getBytes() - done(null, Buffer.from(der, 'binary')) - }) - } catch (err) { - done(err) - } - }) + // convert message to DER + const der = forge.asn1.toDer(p7.toAsn1()).getBytes() + return Buffer.from(der, 'binary') } /** @@ -87,24 +66,20 @@ class CMS { * exists, an Error is returned with the property 'missingKeys'. It is array of key ids. * * @param {Buffer} cmsData - The CMS encrypted data to decrypt. - * @param {function(Error, Buffer)} callback * @returns {undefined} */ - decrypt (cmsData, callback) { - const done = (err, result) => setImmediate(() => callback(err, result)) - + async decrypt (cmsData) { if (!Buffer.isBuffer(cmsData)) { - return done(errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS')) + throw errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS') } - const self = this let cms try { const buf = forge.util.createBuffer(cmsData.toString('binary')) const obj = forge.asn1.fromDer(buf) cms = forge.pkcs7.messageFromAsn1(obj) } catch (err) { - return done(errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS')) + throw errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS') } // Find a recipient whose key we hold. We only deal with recipient certs @@ -118,31 +93,29 @@ class CMS { keyId: r.issuer.find(a => a.shortName === 'CN').value } }) - detect( - recipients, - (r, cb) => self.keychain.findKeyById(r.keyId, (err, info) => cb(null, !err && info)), - (err, r) => { - if (err) return done(err) - if (!r) { - const missingKeys = recipients.map(r => r.keyId) - err = errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', { - missingKeys - }) - return done(err) - } - waterfall([ - (cb) => self.keychain.findKeyById(r.keyId, cb), - (key, cb) => self.keychain._getPrivateKey(key.name, cb) - ], (err, pem) => { - if (err) return done(err) - - const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._()) - cms.decrypt(r.recipient, privateKey) - done(null, Buffer.from(cms.content.getBytes(), 'binary')) - }) + const r = await findAsync(recipients, async (recipient) => { + try { + const key = await this.keychain.findKeyById(recipient.keyId) + if (key) return true + } catch (err) { + return false } - ) + return false + }) + + if (!r) { + const missingKeys = recipients.map(r => r.keyId) + throw errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', { + missingKeys + }) + } + + const key = await this.keychain.findKeyById(r.keyId) + const pem = await this.keychain._getPrivateKey(key.name) + const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._()) + cms.decrypt(r.recipient, privateKey) + return Buffer.from(cms.content.getBytes(), 'binary') } } diff --git a/src/keychain.js b/src/keychain.js index f8f8889..2f67345 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,8 +5,6 @@ const sanitize = require('sanitize-filename') const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') -const collect = require('pull-stream/sinks/collect') -const pull = require('pull-stream/pull') const CMS = require('./cms') const errcode = require('err-code') @@ -37,22 +35,21 @@ function validateKeyName (name) { } /** - * Returns an error to the caller, after a delay + * 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 {function(Error)} callback - The caller * @param {string | Error} err - The error - * @returns {undefined} * @private */ -function _error (callback, err) { +async function throwDelayed (err) { const min = 200 const max = 1000 const delay = Math.random() * (max - min) + min - setTimeout(callback, delay, err, null) + await new Promise(resolve => setTimeout(resolve, delay)) + throw err } /** @@ -175,146 +172,131 @@ class Keychain { * @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. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - createKey (name, type, size, callback) { + async createKey (name, type, size) { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (typeof type !== 'string') { - return _error(callback, errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE')) + return throwDelayed(errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE')) } if (!Number.isSafeInteger(size)) { - return _error(callback, errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE')) + return throwDelayed(errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE')) } const dsname = DsName(name) - self.store.has(dsname, (err, exists) => { - if (err) return _error(callback, err) - if (exists) return _error(callback, errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - switch (type.toLowerCase()) { - case 'rsa': - if (size < 2048) { - return _error(callback, errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE')) - } - break - default: - break + 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)) - crypto.keys.generateKeyPair(type, size, (err, keypair) => { - if (err) return _error(callback, err) - keypair.id((err, kid) => { - if (err) return _error(callback, err) - keypair.export(this._(), (err, pem) => { - if (err) return _error(callback, err) - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - batch.commit((err) => { - if (err) return _error(callback, err) - - callback(null, keyInfo) - }) - }) - }) - }) - }) + await batch.commit() + } catch (err) { + return throwDelayed(err) + } + + return keyInfo } /** * List all the keys. * - * @param {function(Error, KeyInfo[])} callback - * @returns {undefined} + * @returns {KeyInfo[]} */ - listKeys (callback) { + async listKeys () { const self = this const query = { prefix: infoPrefix } - pull( - self.store.query(query), - collect((err, res) => { - if (err) return _error(callback, err) - const info = res.map(r => JSON.parse(r.value)) - callback(null, info) - }) - ) + 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. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - findKeyById (id, callback) { - this.listKeys((err, keys) => { - if (err) return _error(callback, err) - - const key = keys.find((k) => k.id === id) - callback(null, key) - }) + 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. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - findKeyByName (name, callback) { + async findKeyByName (name) { if (!validateKeyName(name)) { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } const dsname = DsInfoName(name) - this.store.get(dsname, (err, res) => { - if (err) { - return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } - - callback(null, JSON.parse(res.toString())) - }) + 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. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - removeKey (name, callback) { + async removeKey (name) { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } const dsname = DsName(name) - self.findKeyByName(name, (err, keyinfo) => { - if (err) return _error(callback, err) - const batch = self.store.batch() - batch.delete(dsname) - batch.delete(DsInfoName(name)) - batch.commit((err) => { - if (err) return _error(callback, err) - callback(null, keyinfo) - }) - }) + const keyInfo = await self.findKeyByName(name) + const batch = self.store.batch() + batch.delete(dsname) + batch.delete(DsInfoName(name)) + await batch.commit() + return keyInfo } /** @@ -322,47 +304,41 @@ class Keychain { * * @param {string} oldName - The old local key name; must already exist. * @param {string} newName - The new local key name; must not already exist. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - renameKey (oldName, newName, callback) { + async renameKey (oldName, newName) { const self = this if (!validateKeyName(oldName) || oldName === 'self') { - return _error(callback, errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID')) + return throwDelayed(errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID')) } if (!validateKeyName(newName) || newName === 'self') { - return _error(callback, errcode(new Error(`Invalid new key name '${newName}'`), 'ERR_NEW_KEY_NAME_INVALID')) + 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) - this.store.get(oldDsname, (err, res) => { - if (err) { - return _error(callback, errcode(new Error(`Key '${oldName}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } + + 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() - self.store.has(newDsname, (err, exists) => { - if (err) return _error(callback, err) - if (exists) return _error(callback, errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - self.store.get(oldInfoName, (err, res) => { - if (err) return _error(callback, err) - - 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) - batch.commit((err) => { - if (err) return _error(callback, err) - callback(null, keyInfo) - }) - }) - }) - }) + 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) + } } /** @@ -370,28 +346,25 @@ class Keychain { * * @param {string} name - The local key name; must already exist. * @param {string} password - The password - * @param {function(Error, string)} callback - * @returns {undefined} + * @returns {string} */ - exportKey (name, password, callback) { + async exportKey (name, password) { if (!validateKeyName(name)) { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!password) { - return _error(callback, errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED')) + return throwDelayed(errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED')) } const dsname = DsName(name) - this.store.get(dsname, (err, res) => { - if (err) { - return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } + try { + const res = await this.store.get(dsname) const pem = res.toString() - crypto.keys.import(pem, this._(), (err, privateKey) => { - if (err) return _error(callback, err) - privateKey.export(password, callback) - }) - }) + const privateKey = await crypto.keys.import(pem, this._()) + return privateKey.export(password) + } catch (err) { + return throwDelayed(err) + } } /** @@ -400,99 +373,97 @@ class Keychain { * @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. - * @param {function(Error, KeyInfo)} callback - * @returns {undefined} + * @returns {KeyInfo} */ - importKey (name, pem, password, callback) { + async importKey (name, pem, password) { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!pem) { - return _error(callback, 'PEM encoded key is required') + return throwDelayed(errcode(new Error('PEM encoded key is required'), 'ERR_PEM_REQUIRED')) } const dsname = DsName(name) - self.store.has(dsname, (err, exists) => { - if (err) return _error(callback, err) - if (exists) return _error(callback, errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - crypto.keys.import(pem, password, (err, privateKey) => { - if (err) return _error(callback, errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY')) - privateKey.id((err, kid) => { - if (err) return _error(callback, err) - privateKey.export(this._(), (err, pem) => { - if (err) return _error(callback, err) - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - batch.commit((err) => { - if (err) return _error(callback, err) - - callback(null, keyInfo) - }) - }) - }) - }) - }) + 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 } - importPeer (name, peer, callback) { + async importPeer (name, peer) { const self = this if (!validateKeyName(name)) { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) + return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!peer || !peer.privKey) { - return _error(callback, errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY')) + return throwDelayed(errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY')) } const privateKey = peer.privKey const dsname = DsName(name) - self.store.has(dsname, (err, exists) => { - if (err) return _error(callback, err) - if (exists) return _error(callback, errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - - privateKey.id((err, kid) => { - if (err) return _error(callback, err) - privateKey.export(this._(), (err, pem) => { - if (err) return _error(callback, err) - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, pem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - batch.commit((err) => { - if (err) return _error(callback, err) - - callback(null, keyInfo) - }) - }) - }) - }) + 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 - * @param {function(Error, string)} callback - * @returns {undefined} + * @returns {string} * @private */ - _getPrivateKey (name, callback) { + async _getPrivateKey (name) { if (!validateKeyName(name)) { - return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_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')) } - this.store.get(DsName(name), (err, res) => { - if (err) { - return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) - } - callback(null, res.toString()) - }) } } diff --git a/src/util.js b/src/util.js index bc61c5b..50ce417 100644 --- a/src/util.js +++ b/src/util.js @@ -14,10 +14,9 @@ exports = module.exports * * @param {KeyInfo} key - The id and name of the key * @param {RsaPrivateKey} privateKey - The naked key - * @param {function(Error, Certificate)} callback * @returns {undefined} */ -exports.certificateForKey = (key, privateKey, callback) => { +exports.certificateForKey = (key, privateKey) => { const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) const cert = pki.createCertificate() cert.publicKey = publicKey @@ -67,5 +66,24 @@ exports.certificateForKey = (key, privateKey, callback) => { // self-sign certificate cert.sign(privateKey) - return callback(null, cert) + return cert } + +/** + * Finds the first item in a collection that is matched in the + * `asyncCompare` function. + * + * `asyncCompare` is an async function that must + * resolve to either `true` or `false`. + * + * @param {Array} array + * @param {function(*)} asyncCompare An async function that returns a boolean + */ +async function findAsync (array, asyncCompare) { + const promises = array.map(asyncCompare) + const results = await Promise.all(promises) + const index = results.findIndex(result => result) + return array[index] +} + +module.exports.findAsync = findAsync diff --git a/test/browser.js b/test/browser.js index 0a37bed..02222fb 100644 --- a/test/browser.js +++ b/test/browser.js @@ -1,25 +1,24 @@ /* eslint-env mocha */ 'use strict' -const series = require('async/series') const LevelStore = require('datastore-level') describe('browser', () => { - const datastore1 = new LevelStore('test-keystore-1', { db: require('level-js') }) - const datastore2 = new LevelStore('test-keystore-2', { db: require('level-js') }) + const datastore1 = new LevelStore('test-keystore-1', { db: require('level') }) + const datastore2 = new LevelStore('test-keystore-2', { db: require('level') }) - before((done) => { - series([ - (cb) => datastore1.open(cb), - (cb) => datastore2.open(cb) - ], done) + before(() => { + return Promise.all([ + datastore1.open(), + datastore2.open() + ]) }) - after((done) => { - series([ - (cb) => datastore1.close(cb), - (cb) => datastore2.close(cb) - ], done) + after(() => { + return Promise.all([ + datastore1.close(), + datastore2.close() + ]) }) require('./keychain.spec')(datastore1, datastore2) diff --git a/test/cms-interop.js b/test/cms-interop.js index a744998..06eb631 100644 --- a/test/cms-interop.js +++ b/test/cms-interop.js @@ -15,14 +15,13 @@ module.exports = (datastore) => { const aliceKeyName = 'cms-interop-alice' let ks - before((done) => { + before(() => { ks = new Keychain(datastore, { passPhrase: passPhrase }) - done() }) const plainData = Buffer.from('This is a message from Alice to Bob') - it('imports openssl key', function (done) { + it('imports openssl key', async function () { this.timeout(10 * 1000) const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA' const alice = `-----BEGIN ENCRYPTED PRIVATE KEY----- @@ -43,15 +42,12 @@ igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL -----END ENCRYPTED PRIVATE KEY----- ` - ks.importKey(aliceKeyName, alice, 'mypassword', (err, key) => { - expect(err).to.not.exist() - expect(key.name).to.equal(aliceKeyName) - expect(key.id).to.equal(aliceKid) - done() - }) + 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', (done) => { + it('decrypts node-forge example', async () => { const example = ` MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI @@ -62,12 +58,9 @@ knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3 DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N ` - ks.cms.decrypt(Buffer.from(example, 'base64'), (err, plain) => { - expect(err).to.not.exist() - expect(plain).to.exist() - expect(plain.toString()).to.equal(plainData.toString()) - done() - }) + 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.spec.js b/test/keychain.spec.js index bcaa667..c455f2d 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -3,11 +3,11 @@ 'use strict' const chai = require('chai') -const dirtyChai = require('dirty-chai') const expect = chai.expect -chai.use(dirtyChai) +const fail = expect.fail +chai.use(require('dirty-chai')) chai.use(require('chai-string')) -const Keychain = require('..') +const Keychain = require('../') const PeerId = require('peer-id') module.exports = (datastore1, datastore2) => { @@ -55,148 +55,111 @@ module.exports = (datastore1, datastore2) => { }) describe('key name', () => { - it('is a valid filename and non-ASCII', () => { - ks.removeKey('../../nasty', (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid key name \'../../nasty\'') - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - ks.removeKey('', (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid key name \'\'') - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - ks.removeKey(' ', (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid key name \' \'') - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - ks.removeKey(null, (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid key name \'null\'') - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - }) - ks.removeKey(undefined, (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid key name \'undefined\'') - expect(err).to.have.property('code', 'ERR_INVALID_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', function (done) { - this.timeout(50 * 1000) - ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => { - expect(err).to.not.exist() - expect(info).exist() - rsaKeyInfo = info - done() - }) - }) - - it('has a name and id', () => { + 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', (done) => { - ks._getPrivateKey(rsaKeyName, (err, pem) => { - expect(err).to.not.exist() - expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - done() - }) + it('is encrypted PEM encoded PKCS #8', async () => { + const pem = await ks._getPrivateKey(rsaKeyName) + return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') }) - it('does not overwrite existing key', (done) => { - ks.createKey(rsaKeyName, 'rsa', 2048, (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') - done() - }) + 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('cannot create the "self" key', (done) => { - ks.createKey('self', 'rsa', 2048, (err) => { - expect(err).to.exist() - done() - }) + 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('should validate name is string', (done) => { - ks.createKey(5, 'rsa', 2048, (err) => { - expect(err).to.exist() - expect(err.message).to.contain('Invalid key name') - done() - }) + 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('should validate type is string', (done) => { - ks.createKey('TEST' + Date.now(), null, 2048, (err) => { - expect(err).to.exist() - expect(err.message).to.contain('Invalid key type') - done() - }) + 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 size is integer', (done) => { - ks.createKey('TEST' + Date.now(), 'rsa', 'string', (err) => { - expect(err).to.exist() - expect(err.message).to.contain('Invalid key size') - done() - }) + 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', (done) => { - ks.createKey('bad-nist-rsa', 'rsa', 1024, (err) => { - expect(err).to.exist() - expect(err).to.have.property('message', 'Invalid RSA key size 1024') - expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE') - done() - }) + 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', (done) => { - ks.listKeys((err, keys) => { - expect(err).to.not.exist() - expect(keys).to.exist() - const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) - expect(mykey).to.exist() - done() - }) + 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', (done) => { - ks.findKeyByName(rsaKeyName, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - done() - }) + 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', (done) => { - ks.findKeyById(rsaKeyInfo.id, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) - done() - }) + 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', (done) => { - ks.listKeys((err, keys) => { - expect(err).to.not.exist() - expect(keys).to.exist() - keys.forEach((key) => { - expect(key).to.have.property('name') - expect(key).to.have.property('id') - }) - done() + 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') }) }) }) @@ -205,103 +168,97 @@ module.exports = (datastore1, datastore2) => { const plainData = Buffer.from('This is a message from Alice to Bob') let cms - it('service is available', (done) => { + it('service is available', () => { expect(ks).to.have.property('cms') - done() }) - it('requires a key', (done) => { - ks.cms.encrypt('no-key', plainData, (err, msg) => { - expect(err).to.exist() - done() - }) + 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', (done) => { - ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => { - expect(err).to.exist() - done() - }) + 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', (done) => { - ks.cms.encrypt(rsaKeyName, plainData, (err, msg) => { - expect(err).to.not.exist() - expect(msg).to.exist() - expect(msg).to.be.instanceOf(Buffer) - cms = msg - done() - }) + 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', (done) => { - ks.cms.decrypt('not CMS', (err) => { - expect(err).to.exist() - done() - }) + 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', (done) => { - ks.cms.decrypt(plainData, (err) => { - expect(err).to.exist() - done() - }) + 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', (done) => { - emptyKeystore.cms.decrypt(cms, (err, plain) => { - 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') - done() - }) + 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', (done) => { - ks.cms.decrypt(cms, (err, plain) => { - expect(err).to.not.exist() - expect(plain).to.exist() - expect(plain.toString()).to.equal(plainData.toString()) - done() - }) + 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('is a PKCS #8 encrypted pem', (done) => { - ks.exportKey(rsaKeyName, 'password', (err, pem) => { - expect(err).to.not.exist() - expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - pemKey = pem - done() - }) + 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('can be imported', (done) => { - ks.importKey('imported-key', pemKey, 'password', (err, key) => { - expect(err).to.not.exist() - expect(key.name).to.equal('imported-key') - expect(key.id).to.equal(rsaKeyInfo.id) - done() - }) + 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('cannot be imported as an existing key name', (done) => { - ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => { - expect(err).to.exist() - done() - }) + 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 imported with the wrong password', function (done) { - this.timeout(5 * 1000) - ks.importKey('a-new-name-for-import', pemKey, 'not the password', (err, key) => { - expect(err).to.exist() - done() - }) + 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') }) }) @@ -309,136 +266,117 @@ module.exports = (datastore1, datastore2) => { 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(function (done) { + before(async function () { const encoded = Buffer.from(alicePrivKey, 'base64') - PeerId.createFromPrivKey(encoded, (err, id) => { - expect(err).to.not.exist() - alice = id - done() - }) + alice = await PeerId.createFromPrivKey(encoded) }) - it('private key can be imported', (done) => { - ks.importPeer('alice', alice, (err, key) => { - expect(err).to.not.exist() - expect(key.name).to.equal('alice') - expect(key.id).to.equal(alice.toB58String()) - done() - }) + 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('key id exists', (done) => { - ks.findKeyById(alice.toB58String(), (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', 'alice') - expect(key).to.have.property('id', alice.toB58String()) - done() - }) + 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('key name exists', (done) => { - ks.findKeyByName('alice', (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', 'alice') - expect(key).to.have.property('id', alice.toB58String()) - done() - }) + 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', (done) => { - ks.renameKey('not-there', renamedRsaKeyName, (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') - done() - }) + 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', (done) => { - ks.renameKey(rsaKeyName, '..\not-valid', (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') - done() - }) + 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', (done) => { - ks.renameKey(rsaKeyName, rsaKeyName, (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS') - done() - }) + 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', (done) => { - ks.renameKey(rsaKeyName, 'self', (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID') - done() - }) + 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', (done) => { - ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - ks.findKeyByName(rsaKeyName, (err, key) => { - expect(err).to.exist() - done() - }) - }) + 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', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - done() - }) + 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', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - done() - }) + 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', (done) => { - ks.removeKey('self', (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME') - done() - }) + 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', (done) => { - ks.removeKey('not-there', (err) => { - expect(err).to.exist() - expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND') - done() - }) + 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', (done) => { - ks.removeKey(renamedRsaKeyName, (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) - expect(key).to.have.property('id', rsaKeyInfo.id) - done() - }) + 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/node.js b/test/node.js index e11d074..f67b6b6 100644 --- a/test/node.js +++ b/test/node.js @@ -3,8 +3,8 @@ const os = require('os') const path = require('path') -const rimraf = require('rimraf') -const series = require('async/series') +const promisify = require('promisify-es6') +const rimraf = promisify(require('rimraf')) const FsStore = require('datastore-fs') describe('node', () => { @@ -13,20 +13,16 @@ describe('node', () => { const datastore1 = new FsStore(store1) const datastore2 = new FsStore(store2) - before((done) => { - series([ - (cb) => datastore1.open(cb), - (cb) => datastore2.open(cb) - ], done) + before(async () => { + await datastore1.open() + await datastore2.open() }) - after((done) => { - series([ - (cb) => datastore1.close(cb), - (cb) => datastore2.close(cb), - (cb) => rimraf(store1, cb), - (cb) => rimraf(store2, cb) - ], done) + after(async () => { + await datastore1.close() + await datastore2.close() + await rimraf(store1) + await rimraf(store2) }) require('./keychain.spec')(datastore1, datastore2) diff --git a/test/peerid.js b/test/peerid.js index 7d6588c..74ba9bf 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -21,55 +21,32 @@ describe('peer ID', () => { let peer let publicKeyDer // a buffer - before(function (done) { + before(async () => { const encoded = Buffer.from(sample.privKey, 'base64') - PeerId.createFromPrivKey(encoded, (err, id) => { - expect(err).to.not.exist() - peer = id - done() - }) + peer = await PeerId.createFromPrivKey(encoded) }) - it('decoded public key', (done) => { - // console.log('peer id', peer.toJSON()) - // console.log('id', peer.toB58String()) - // console.log('id decoded', multihash.decode(peer.id)) - + it('decoded public key', () => { // get protobuf version of the public key const publicKeyProtobuf = peer.marshalPubKey() const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf) - // console.log('public key', publicKey) publicKeyDer = publicKey.marshal() - // console.log('public key der', publicKeyDer.toString('base64')) // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { - expect(err).to.not.exist() - // console.log('private key', key) - // console.log('\nprivate key der', key.marshal().toString('base64')) - done() - }) + const key = crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() }) - it('encoded public key with DER', (done) => { + it('encoded public key with DER', async () => { const jwk = rsaUtils.pkixToJwk(publicKeyDer) - // console.log('jwk', jwk) const rsa = new rsaClass.RsaPublicKey(jwk) - // console.log('rsa', rsa) - rsa.hash((err, keyId) => { - expect(err).to.not.exist() - // console.log('err', err) - // console.log('keyId', keyId) - // console.log('id decoded', multihash.decode(keyId)) - const kids = multihash.toB58String(keyId) - // console.log('id', kids) - expect(kids).to.equal(peer.toB58String()) - done() - }) + const keyId = await rsa.hash() + const kids = multihash.toB58String(keyId) + expect(kids).to.equal(peer.toB58String()) }) - it('encoded public key with JWT', (done) => { + it('encoded public key with JWT', async () => { const jwk = { kty: 'RSA', n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw', @@ -77,33 +54,16 @@ describe('peer ID', () => { alg: 'RS256', kid: '2011-04-29' } - // console.log('jwk', jwk) const rsa = new rsaClass.RsaPublicKey(jwk) - // console.log('rsa', rsa) - rsa.hash((err, keyId) => { - expect(err).to.not.exist() - // console.log('err', err) - // console.log('keyId', keyId) - // console.log('id decoded', multihash.decode(keyId)) - const kids = multihash.toB58String(keyId) - // console.log('id', kids) - expect(kids).to.equal(peer.toB58String()) - done() - }) + const keyId = await rsa.hash() + const kids = multihash.toB58String(keyId) + expect(kids).to.equal(peer.toB58String()) }) - it('decoded private key', (done) => { - // console.log('peer id', peer.toJSON()) - // console.log('id', peer.toB58String()) - // console.log('id decoded', multihash.decode(peer.id)) - + it('decoded private key', async () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { - expect(err).to.not.exist() - // console.log('private key', key) - // console.log('\nprivate key der', key.marshal().toString('base64')) - done() - }) + const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() }) })