From 4c8d147c92bada8d535931d64aa297997361a445 Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 6 Dec 2017 08:24:22 +0000 Subject: [PATCH 01/87] Initial commit --- .gitignore | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++++++++++ README.md | 1 + 3 files changed, 81 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..00cbbdf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bbfffbf9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 libp2p + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..b45d8333 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# js-libp2p-keychain \ No newline at end of file From 49e6c47c40924ad1d0b45be755f861547ca804aa Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 6 Dec 2017 08:49:16 +0000 Subject: [PATCH 02/87] chore: setup repo --- .gitignore | 54 ++++++++++++++++------------------------------ .travis.yml | 32 +++++++++++++++++++++++++++ README.md | 34 ++++++++++++++++++++++++++++- appveyor.yml | 26 ++++++++++++++++++++++ circle.yml | 18 ++++++++++++++++ package.json | 44 +++++++++++++++++++++++++++++++++++++ src/index.js | 1 + test/index.spec.js | 4 ++++ 8 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 .travis.yml create mode 100644 appveyor.yml create mode 100644 circle.yml create mode 100644 package.json create mode 100644 src/index.js create mode 100644 test/index.spec.js diff --git a/.gitignore b/.gitignore index 00cbbdf5..1c73b378 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,19 @@ +docs +**/node_modules/ +**/*.log +test/repo-tests* +**/bundle.js + # Logs logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* + +coverage # Runtime data pids *.pid *.seed -*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -17,43 +21,23 @@ lib-cov # Coverage directory used by tools like istanbul coverage -# nyc test coverage -.nyc_output - # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt -# Bower dependency directory (https://bower.io/) -bower_components - # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +build -# Dependency directories -node_modules/ -jspm_packages/ +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env +lib +dist +test/test-data/go-ipfs-repo/LOCK +test/test-data/go-ipfs-repo/LOG +test/test-data/go-ipfs-repo/LOG.old +# while testing npm5 +package-lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..584f308f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +sudo: false +language: node_js + +matrix: + include: + - node_js: 6 + env: CXX=g++-4.8 + - node_js: 8 + env: CXX=g++-4.8 + # - node_js: stable + # env: CXX=g++-4.8 + +script: + - npm run lint + - npm run test + - npm run coverage + - make test + +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + +after_success: + - npm run coverage-publish + +addons: + firefox: 'latest' + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/README.md b/README.md index b45d8333..95658501 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ -# js-libp2p-keychain \ No newline at end of file +# 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) +[![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) +[![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) + +> Keychain primitives for libp2p in JavaScript + +## Table of Contents + +## Install + +## API + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/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/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..ba93339b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,26 @@ +environment: + matrix: + - nodejs_version: "6" + - nodejs_version: "8" + +init: + - git config --global core.autocrlf input + +# cache: +# - node_modules + +platform: + - x64 + +install: + - ps: Install-Product node $env:nodejs_version $env:platform + - node --version + - npm --version + - npm install + +test_script: + - npm test + +build: off + +version: "{build}" diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..d67b6ae7 --- /dev/null +++ b/circle.yml @@ -0,0 +1,18 @@ +machine: + node: + version: stable + +test: + post: + - npm run coverage -- --upload + +dependencies: + pre: + - google-chrome --version + - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + - sudo dpkg -i google-chrome.deb || true + - sudo apt-get update + - sudo apt-get install -f + - sudo apt-get install --only-upgrade lsb-base + - sudo dpkg -i google-chrome.deb + - google-chrome --version diff --git a/package.json b/package.json new file mode 100644 index 00000000..35ffcf5d --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "libp2p-keychain", + "version": "0.0.0", + "description": "", + "main": "src/index.js", + "scripts": { + "lint": "aegir lint", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:browser": "aegir test -t browser -t webworker", + "release": "aegir release", + "release-minor": "aegir release --type minor", + "release-major": "aegir release --type major" + }, + "pre-commit": [ + "lint", + "test" + ], + "engines": { + "node": ">=6.0.0", + "npm": ">=3.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-keychain.git" + }, + "keywords": [ + "IPFS", + "libp2p", + "keys", + "crypto" + ], + "author": "David Dias ", + "license": "MIT", + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-keychain/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", + "devDependencies": { + "aegir": "^12.2.0", + "pre-commit": "^1.2.2" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..ccacec30 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +'use strict' diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 00000000..c638cf86 --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,4 @@ +/* eslint-env mocha */ +'use strict' + +it('so much testing', () => {}) From 1a96ae8cb73396ec7fb64edb9897587697bbe026 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Wed, 6 Dec 2017 22:56:09 +1300 Subject: [PATCH 03/87] feat: move bits from https://github.com/richardschneider/ipfs-encryption --- .gitattributes | 2 + README.md | 79 ++++++++- doc/private-key.png | Bin 0 -> 25518 bytes doc/private-key.xml | 1 + package.json | 29 +++- src/cms.js | 97 +++++++++++ src/index.js | 2 + src/keychain.js | 362 ++++++++++++++++++++++++++++++++++++++++++ src/util.js | 86 ++++++++++ test/browser.js | 30 ++++ test/index.spec.js | 4 - test/keychain.spec.js | 356 +++++++++++++++++++++++++++++++++++++++++ test/node.js | 34 ++++ test/peerid.js | 105 ++++++++++++ 14 files changed, 1178 insertions(+), 9 deletions(-) create mode 100644 .gitattributes create mode 100644 doc/private-key.png create mode 100644 doc/private-key.xml create mode 100644 src/cms.js create mode 100644 src/keychain.js create mode 100644 src/util.js create mode 100644 test/browser.js delete mode 100644 test/index.spec.js create mode 100644 test/keychain.spec.js create mode 100644 test/node.js create mode 100644 test/peerid.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ef41d4fa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.png binary +* crlf=input diff --git a/README.md b/README.md index 95658501..0f89dcdb 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,91 @@ ![](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) -> Keychain primitives for libp2p in JavaScript +> A secure key chain for libp2p in JavaScript + +## Features + +- Manages the lifecycle of a key +- Keys are encrypted at rest +- Enforces the use of safe key names +- Uses encrypted PKCS 8 for key storage +- Uses PBKDF2 for a "stetched" key encryption key +- Enforces NIST SP 800-131A and NIST SP 800-132 +- 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 +### Usage + + const datastore = new FsStore('./a-keystore') + const opts = { + passPhrase: 'some long easily remembered phrase' + } + const keychain = new Keychain(datastore, opts) + ## API +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)` + +A naming service for a key + +- `listKeys (callback)` +- `findKeyById (id, callback)` +- `findKeyByName (name, callback)` + +Cryptographically protected messages + +- `cms.createAnonymousEncryptedData (name, plain, callback)` +- `cms.readData (cmsData, callback)` + +### 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. + +``` +{ + name: 'rsa-key', + id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW' +} +``` + +The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt). + +### Private key storage + +A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. Its file extension is `.p8`. + +The default options for generating the derived encryption key are in the `dek` object +``` +const defaultOptions = { + createIfNeeded: true, + + //See https://cryptosense.com/parameter-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + salt: 'you should override this value with a crypto secure random number', + hash: 'sha512' + } +} +``` + +![key storage](../doc/private-key.png?raw=true) + +### Physical storage + +The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benifit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)! diff --git a/doc/private-key.png b/doc/private-key.png new file mode 100644 index 0000000000000000000000000000000000000000..4c85dc610c883942212ff3b419cad3cae3c24769 GIT binary patch literal 25518 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 NO7iM*Wil3F{{{GIyypM_ literal 0 HcmV?d00001 diff --git a/doc/private-key.xml b/doc/private-key.xml new file mode 100644 index 00000000..51cb8c5a --- /dev/null +++ b/doc/private-key.xml @@ -0,0 +1 @@ +7VlNb6MwEP01HLfCGBJ6bNJ2V9pdqVIP2x4dcMAKYGScJumvXxNsvkw+SmgSVe2hMs9mbL839swQA07j9U+G0vAv9XFkWKa/NuC9YVmua4n/ObApAOjCAggY8QsIVMAzeccSNCW6JD7OGgM5pREnaRP0aJJgjzcwxBhdNYfNadScNUUB1oBnD0U6+o/4PJTbssYV/guTIFQzg9Ft0TND3iJgdJnI+QwLzrd/RXeMlC250SxEPl3VIPhgwCmjlBeteD3FUU6toq1473FHb7luhhN+zAtSpzcULeXWU5RluYmQoQzLRfKNIobjtbA7CXkcCQCIZsYZXeApjSgTSEITMXIyJ1HUglBEgkQ8emJlWOCTN8w4EZTfyY6Y+H4+zWQVEo6fU+Tlc66EfwlsSynOF22KJ7loYQCvd24clHQKL8U0xpxtxBDlolIA6aBgJJ9Xldy2hMKa0ko3JB0sKA1XJIuG5Lmbc6hx/jT5ff9oaWQL50jzZsqoh4Uq3dTUtBiAF9AmxtaJAVYHM6MBmLE1Zny8EABNOaFJ9nW9sfQryfr4fN7oaJxrNOPEv8sv1ZyvSFwPxGuSLjbJNi85GzcmGCvgdQvAUQk8YUbE8nK6a7xhX7uKD7JWo8XpoEVhDEeIk7em+S6u5AxPlIiJq6PQEgWMraaJjC6Zh+Vb9Uu2bUiFw12GOGIB5pqhrXTlto9SczSomk5Dyw9IJsL1dku1C+9SKpYHR5Fvmj1VhE1D2ukbTkX3WlQsuGmErbqw4KLnE5oHBDlWWbt10K22i+xQVgiANrVhaT4g271g22xfKI3kTDQKi33d5rY7fB4Mmgxn5B3NtgNy/5D7EKOdieHcfyhcRmiGo0mZBauwW+XBe+KlzOblSoxSz7pjunvj6A8RgcpaY9Mw3tfZ1BA6n2f41IOt6puaRAucrz/AiSbUNaR/Fjxj+geAxk668PJqRLiPexX8QPuS/OjVmo84yjhleqV2CXac9o18Vnb06uEm3e01PvWW8XZfh4iZFdn+n9mQTLWSCQhcjanRntB5ElF6yl9cQl++zGpfbo7unp9VZgE9M2dJoFFdbRmc5cRarRMLLd0P3S5KnAEoGWuUaHwcTHPXhL/U2q/NjPdF+k6tIHV6J8AqeF9PBtzyZxu2HLVvaQPdlqHhShswaG0zmLQdVWsRbb+lPV5avf44Qdpm2Vo/67JLnfb+oo86RDeNKxLdHkr0208TXcXGz/pW0S066C+61SG6/S36x0TXC7VTRP9SH43VLahyzHZpc/xHY7DfUG85xWP1A2MxvPoRFz78Bw== \ No newline at end of file diff --git a/package.json b/package.json index 35ffcf5d..91167499 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libp2p-keychain", - "version": "0.0.0", - "description": "", + "version": "0.1.0", + "description": "Key management and cryptographically protected messages", "main": "src/index.js", "scripts": { "lint": "aegir lint", @@ -29,16 +29,37 @@ "IPFS", "libp2p", "keys", + "encryption", + "secure", "crypto" ], - "author": "David Dias ", + "author": "Richard Schneider ", "license": "MIT", "bugs": { "url": "https://github.com/libp2p/js-libp2p-keychain/issues" }, "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", + "dependencies": { + "async": "^2.6.0", + "deepmerge": "^1.5.2", + "interface-datastore": "~0.4.1", + "libp2p-crypto": "~0.10.3", + "multihashes": "~0.4.12", + "node-forge": "~0.7.1", + "pull-stream": "^3.6.1", + "sanitize-filename": "^1.6.1" + }, "devDependencies": { "aegir": "^12.2.0", - "pre-commit": "^1.2.2" + "chai": "^4.1.2", + "chai-string": "^1.4.0", + "datastore-fs": "^0.4.1", + "datastore-level": "^0.7.0", + "dirty-chai": "^2.0.1", + "level-js": "^2.2.4", + "mocha": "^4.0.1", + "peer-id": "^0.10.2", + "pre-commit": "^1.2.2", + "rimraf": "^2.6.2" } } diff --git a/src/cms.js b/src/cms.js new file mode 100644 index 00000000..2f2d9c7d --- /dev/null +++ b/src/cms.js @@ -0,0 +1,97 @@ +'use strict' + +const async = require('async') +const forge = require('node-forge') +const util = require('./util') + +class CMS { + constructor (keystore) { + if (!keystore) { + throw new Error('keystore is required') + } + + this.keystore = keystore; + } + + createAnonymousEncryptedData (name, plain, callback) { + const self = this + if (!Buffer.isBuffer(plain)) { + return callback(new Error('Data is required')) + } + + self.keystore._getPrivateKey(name, (err, key) => { + if (err) { + return callback(err) + } + + try { + const privateKey = forge.pki.decryptRsaPrivateKey(key, self.keystore._()) + util.certificateForKey(privateKey, (err, certificate) => { + if (err) return callback(err) + + // 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() + callback(null, Buffer.from(der, 'binary')) + }) + } catch (err) { + callback(err) + } + }) + } + + readData (cmsData, callback) { + if (!Buffer.isBuffer(cmsData)) { + return callback(new Error('CMS data is required')) + } + + 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 callback(new Error('Invalid CMS: ' + err.message)) + } + + // Find a recipient whose key we hold. We only deal with recipient certs + // issued by ipfs (O=ipfs). + const recipients = cms.recipients + .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs')) + .filter(r => r.issuer.find(a => a.shortName === 'CN')) + .map(r => { + return { + recipient: r, + keyId: r.issuer.find(a => a.shortName === 'CN').value + } + }) + async.detect( + recipients, + (r, cb) => self.keystore.findKeyById(r.keyId, (err, info) => cb(null, !err && info)), + (err, r) => { + if (err) return callback(err) + if (!r) return callback(new Error('No key found for decryption')) + + async.waterfall([ + (cb) => self.keystore.findKeyById(r.keyId, cb), + (key, cb) => self.keystore._getPrivateKey(key.name, cb) + ], (err, pem) => { + if (err) return callback(err); + + const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._()) + cms.decrypt(r.recipient, privateKey) + async.setImmediate(() => callback(null, Buffer.from(cms.content.getBytes(), 'binary'))) + }) + } + ) + } + +} + +module.exports = CMS diff --git a/src/index.js b/src/index.js index ccacec30..2704d626 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,3 @@ 'use strict' + +module.exports = require('./keychain') diff --git a/src/keychain.js b/src/keychain.js new file mode 100644 index 00000000..50a67983 --- /dev/null +++ b/src/keychain.js @@ -0,0 +1,362 @@ +'use strict' + +const async = require('async') +const sanitize = require("sanitize-filename") +const forge = require('node-forge') +const deepmerge = require('deepmerge') +const crypto = require('crypto') +const libp2pCrypto = require('libp2p-crypto') +const util = require('./util') +const CMS = require('./cms') +const DS = require('interface-datastore') +const pull = require('pull-stream') + +const keyExtension = '.p8' + +// 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: 'sha512' + } +} + +function validateKeyName (name) { + if (!name) return false + + return name === sanitize(name.trim()) +} + +/** + * Returns an error to the caller, 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 + */ +function _error(callback, err) { + const min = 200 + const max = 1000 + const delay = Math.random() * (max - min) + min + if (typeof err === 'string') err = new Error(err) + setTimeout(callback, delay, err, null) +} + +/** + * Converts a key name into a datastore name. + */ +function DsName (name) { + return new DS.Key('/' + name) +} + +/** + * Converts a datastore name into a key name. + */ +function KsName(name) { + return name.toString().slice(1) +} + +class Keychain { + constructor (store, options) { + if (!store) { + throw new Error('store is required') + } + this.store = store + if (this.store.opts) { + this.store.opts.extension = keyExtension + } + + const opts = deepmerge(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}`) + } + this.dek = opts.dek + + // Create the derived encrypting key + let dek = forge.pkcs5.pbkdf2( + opts.passPhrase, + opts.dek.salt, + opts.dek.iterationCount, + opts.dek.keyLength, + opts.dek.hash) + dek = forge.util.bytesToHex(dek) + Object.defineProperty(this, '_', { value: () => dek }) + + // JS magick + this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this) + + // Provide access to protected messages + this.cms = new CMS(this) + } + + static get options() { + return defaultOptions + } + + createKey (name, type, size, callback) { + const self = this + + if (!validateKeyName(name) || name === 'self') { + return _error(callback, `Invalid key name '${name}'`) + } + const dsname = DsName(name) + self.store.has(dsname, (err, exists) => { + if (exists) return _error(callback, `Key '${name}' already exists'`) + + switch (type.toLowerCase()) { + case 'rsa': + if (size < 2048) { + return _error(callback, `Invalid RSA key size ${size}`) + } + forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => { + if (err) return _error(callback, err) + + const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()); + return self.store.put(dsname, pem, (err) => { + if (err) return _error(callback, err) + + self._getKeyInfo(name, callback) + }) + }) + break; + + default: + return _error(callback, `Invalid key type '${type}'`) + } + }) + } + + listKeys (callback) { + const self = this + const query = { + keysOnly: true + } + pull( + self.store.query(query), + pull.collect((err, res) => { + if (err) return _error(callback, err) + + const names = res.map(r => KsName(r.key)) + async.map(names, self._getKeyInfo, callback) + }) + ) + } + + // TODO: not very efficent. + findKeyById (id, callback) { + this.listKeys((err, keys) => { + if (err) return _error(callback, err) + + const key = keys.find((k) => k.id === id) + callback(null, key) + }) + } + + removeKey (name, callback) { + const self = this + if (!validateKeyName(name) || name === 'self') { + return _error(callback, `Invalid key name '${name}'`) + } + const dsname = DsName(name) + self.store.has(dsname, (err, exists) => { + if (!exists) return _error(callback, `Key '${name}' does not exist'`) + + self.store.delete(dsname, callback) + }) + } + + renameKey(oldName, newName, callback) { + const self = this + if (!validateKeyName(oldName) || oldName === 'self') { + return _error(callback, `Invalid old key name '${oldName}'`) + } + if (!validateKeyName(newName) || newName === 'self') { + return _error(callback, `Invalid new key name '${newName}'`) + } + const oldDsname = DsName(oldName) + const newDsname = DsName(newName) + this.store.get(oldDsname, (err, res) => { + if (err) { + return _error(callback, `Key '${oldName}' does not exist. ${err.message}`) + } + const pem = res.toString() + self.store.has(newDsname, (err, exists) => { + if (exists) return _error(callback, `Key '${newName}' already exists'`) + + const batch = self.store.batch() + batch.put(newDsname, pem) + batch.delete(oldDsname) + batch.commit((err) => { + if (err) return _error(callback, err) + self._getKeyInfo(newName, callback) + }) + }) + }) + } + + exportKey (name, password, callback) { + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + if (!password) { + return _error(callback, 'Password is required') + } + + const dsname = DsName(name) + this.store.get(dsname, (err, res) => { + if (err) { + return _error(callback, `Key '${name}' does not exist. ${err.message}`) + } + const pem = res.toString() + try { + const options = { + algorithm: 'aes256', + count: this.dek.iterationCount, + saltSize: NIST.minSaltLength, + prfAlgorithm: 'sha512' + } + const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._()) + const res = forge.pki.encryptRsaPrivateKey(privateKey, password, options) + return callback(null, res) + } catch (e) { + _error(callback, e) + } + }) + } + + importKey(name, pem, password, callback) { + const self = this + if (!validateKeyName(name) || name === 'self') { + return _error(callback, `Invalid key name '${name}'`) + } + if (!pem) { + return _error(callback, 'PEM encoded key is required') + } + const dsname = DsName(name) + self.store.has(dsname, (err, exists) => { + if (exists) return _error(callback, `Key '${name}' already exists'`) + try { + const privateKey = forge.pki.decryptRsaPrivateKey(pem, password) + if (privateKey === null) { + return _error(callback, 'Cannot read the key, most likely the password is wrong') + } + const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._()); + return self.store.put(dsname, newpem, (err) => { + if (err) return _error(callback, err) + + this._getKeyInfo(name, callback) + }) + } catch (err) { + _error(callback, err) + } + }) + } + + importPeer (name, peer, callback) { + const self = this + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + if (!peer || !peer.privKey) { + return _error(callback, 'Peer.privKey \is required') + } + const dsname = DsName(name) + self.store.has(dsname, (err, exists) => { + if (exists) return _error(callback, `Key '${name}' already exists'`) + + const privateKeyProtobuf = peer.marshalPrivKey() + libp2pCrypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { + try { + const der = key.marshal() + const buf = forge.util.createBuffer(der.toString('binary')); + const obj = forge.asn1.fromDer(buf) + const privateKey = forge.pki.privateKeyFromAsn1(obj) + if (privateKey === null) { + return _error(callback, 'Cannot read the peer private key') + } + const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._()); + return self.store.put(dsname, pem, (err) => { + if (err) return _error(callback, err) + + this._getKeyInfo(name, callback) + }) + } catch (err) { + _error(callback, err) + } + }) + }) + } + + /** + * Gets the private key as PEM encoded PKCS #8 + * + * @param {string} name + * @param {function(Error, string)} callback + */ + _getPrivateKey (name, callback) { + const self = this + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + this.store.get(DsName(name), (err, res) => { + if (err) { + return _error(callback, `Key '${name}' does not exist. ${err.message}`) + } + callback(null, res.toString()) + }) + } + + _getKeyInfo (name, callback) { + const self = this + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + + const dsname = DsName(name) + this.store.get(dsname, (err, res) => { + if (err) { + return _error(callback, `Key '${name}' does not exist. ${err.message}`) + } + const pem = res.toString() + try { + const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._()) + util.keyId(privateKey, (err, kid) => { + if (err) return _error(callback, err) + + const info = { + name: name, + id: kid + } + return callback(null, info) + }) + } catch (e) { + _error(callback, e) + } + }) + } + +} + +module.exports = Keychain diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..c3cd5a1f --- /dev/null +++ b/src/util.js @@ -0,0 +1,86 @@ +'use strict' + +const forge = require('node-forge') +const pki = forge.pki +const multihash = require('multihashes') +const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') +const rsaClass = require('libp2p-crypto/src/keys/rsa-class') + +exports = module.exports + +// Create an IPFS key id; the SHA-256 multihash of a public key. +// See https://github.com/richardschneider/ipfs-encryption/issues/16 +exports.keyId = (privateKey, callback) => { + try { + const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) + const spki = pki.publicKeyToSubjectPublicKeyInfo(publicKey) + const der = new Buffer(forge.asn1.toDer(spki).getBytes(), 'binary') + const jwk = rsaUtils.pkixToJwk(der) + const rsa = new rsaClass.RsaPublicKey(jwk) + rsa.hash((err, kid) => { + if (err) return callback(err) + + const kids = multihash.toB58String(kid) + return callback(null, kids) + }) + } catch (err) { + callback(err) + } +} + +exports.certificateForKey = (privateKey, callback) => { + exports.keyId(privateKey, (err, kid) => { + if (err) return callback(err) + + const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) + const cert = pki.createCertificate(); + cert.publicKey = publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10); + var attrs = [{ + name: 'organizationName', + value: 'ipfs' + }, { + shortName: 'OU', + value: 'keystore' + }, { + name: 'commonName', + value: kid + }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([{ + name: 'basicConstraints', + cA: true + }, { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + emailProtection: true, + timeStamping: true + }, { + name: 'nsCertType', + client: true, + server: true, + email: true, + objsign: true, + sslCA: true, + emailCA: true, + objCA: true + }]); + // self-sign certificate + cert.sign(privateKey) + + return callback(null, cert) + }) +} diff --git a/test/browser.js b/test/browser.js new file mode 100644 index 00000000..a2633bef --- /dev/null +++ b/test/browser.js @@ -0,0 +1,30 @@ +/* eslint-env mocha */ +'use strict' + +const async = require('async') +const LevelStore = require('datastore-level') + +// use in the browser with level.js +const browserStore = new LevelStore('my/db/name', {db: require('level-js')}) + +describe('browser', () => { + const datastore1 = new LevelStore('test-keystore-1', {db: require('level-js')}) + const datastore2 = new LevelStore('test-keystore-2', {db: require('level-js')}) + + before((done) => { + async.series([ + (cb) => datastore1.open(cb), + (cb) => datastore2.open(cb) + ], done) + }) + + after((done) => { + async.series([ + (cb) => datastore1.close(cb), + (cb) => datastore2.close(cb) + ], done) + }) + + require('./keychain.spec')(datastore1, datastore2) + require('./peerid') +}) diff --git a/test/index.spec.js b/test/index.spec.js deleted file mode 100644 index c638cf86..00000000 --- a/test/index.spec.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -it('so much testing', () => {}) diff --git a/test/keychain.spec.js b/test/keychain.spec.js new file mode 100644 index 00000000..cc1048cf --- /dev/null +++ b/test/keychain.spec.js @@ -0,0 +1,356 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +chai.use(require('chai-string')) +const Keychain = require('..') +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 + + before((done) => { + emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) + ks = new Keychain(datastore2, { passPhrase: passPhrase }) + done() + }) + + it('needs a pass phrase to encrypt a key', () => { + expect(() => new Keychain(datastore2)).to.throw() + }) + + it ('needs a NIST SP 800-132 non-weak pass phrase', () => { + expect(() => new Keychain(datastore2, { passPhrase: '< 20 character'})).to.throw() + }) + + it('needs a store to persist a key', () => { + expect(() => new Keychain(null, { passPhrase: passPhrase})).to.throw() + }) + + it('has default options', () => { + expect(Keychain.options).to.exist() + }) + + 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\'') + }) + ks.removeKey('', (err) => { + expect(err).to.exist() + expect(err).to.have.property('message', 'Invalid key name \'\'') + }) + ks.removeKey(' ', (err) => { + expect(err).to.exist() + expect(err).to.have.property('message', 'Invalid key name \' \'') + }) + ks.removeKey(null, (err) => { + expect(err).to.exist() + expect(err).to.have.property('message', 'Invalid key name \'null\'') + }) + ks.removeKey(undefined, (err) => { + expect(err).to.exist() + expect(err).to.have.property('message', 'Invalid key name \'undefined\'') + }) + }) + }) + + describe('key', () => { + it('can be an RSA key', function (done) { + this.timeout(20 * 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', () => { + 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('does not overwrite existing key', (done) => { + ks.createKey(rsaKeyName, 'rsa', 2048, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('cannot create the "self" key', (done) => { + ks.createKey('self', 'rsa', 2048, (err) => { + expect(err).to.exist() + done() + }) + }) + + 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') + done() + }) + }) + }) + + }) + + 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 === rsaKeyName) + expect(mykey).to.exist() + done() + }) + }) + + 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 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('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() + }) + }) + }) + + describe('CMS protected data', () => { + const plainData = Buffer.from('This is a message from Alice to Bob') + let cms + + it('service is available', (done) => { + expect(ks).to.have.property('cms') + done() + }) + + it('is anonymous', (done) => { + ks.cms.createAnonymousEncryptedData(rsaKeyName, plainData, (err, msg) => { + expect(err).to.not.exist() + expect(msg).to.exist() + expect(msg).to.be.instanceOf(Buffer) + cms = msg + done() + }) + }) + + it('is a PKCS #7 message', (done) => { + ks.cms.readData("not CMS", (err) => { + expect(err).to.exist() + done() + }) + }) + + it('is a PKCS #7 binary message', (done) => { + ks.cms.readData(plainData, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('cannot be read without the key', (done) => { + emptyKeystore.cms.readData(cms, (err, plain) => { + expect(err).to.exist() + done() + }) + }) + + it('can be read with the key', (done) => { + ks.cms.readData(cms, (err, plain) => { + expect(err).to.not.exist() + expect(plain).to.exist() + expect(plain.toString()).to.equal(plainData.toString()) + done() + }) + }) + + }) + + 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('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('cannot be imported as an existing key name', (done) => { + ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => { + expect(err).to.exist() + done() + }) + }) + + 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() + }) + }) + }) + + describe('peer id', () => { + const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' + let alice + + before(function (done) { + const encoded = Buffer.from(alicePrivKey, 'base64') + PeerId.createFromPrivKey(encoded, (err, id) => { + alice = id + done() + }) + }) + + 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() + }) + }) + }) + + describe('rename', () => { + it('requires an existing key name', (done) => { + ks.renameKey('not-there', renamedRsaKeyName, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('requires a valid new key name', (done) => { + ks.renameKey(rsaKeyName, '..\not-valid', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('does not overwrite existing key', (done) => { + ks.renameKey(rsaKeyName, rsaKeyName, (err) => { + expect(err).to.exist() + done() + }) + }) + + it('cannot create the "self" key', (done) => { + ks.renameKey(rsaKeyName, 'self', (err) => { + expect(err).to.exist() + done() + }) + }) + + 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('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('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() + }) + }) + }) + + describe('key removal', () => { + it('cannot remove the "self" key', (done) => { + ks.removeKey('self', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('cannot remove an unknown key', (done) => { + ks.removeKey('not-there', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('can remove a known key', (done) => { + ks.removeKey(renamedRsaKeyName, (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + + }) +} diff --git a/test/node.js b/test/node.js new file mode 100644 index 00000000..b003a7c8 --- /dev/null +++ b/test/node.js @@ -0,0 +1,34 @@ +/* eslint-env mocha */ +'use strict' + +const os = require('os') +const path = require('path') +const rimraf = require('rimraf') +const async = require('async') +const FsStore = require('datastore-fs') + +describe('node', () => { + const store1 = path.join(os.tmpdir(), 'test-keystore-1') + const store2 = path.join(os.tmpdir(), 'test-keystore-2') + const datastore1 = new FsStore(store1) + const datastore2 = new FsStore(store2) + + before((done) => { + async.series([ + (cb) => datastore1.open(cb), + (cb) => datastore2.open(cb) + ], done) + }) + + after((done) => { + async.series([ + (cb) => datastore1.close(cb), + (cb) => datastore2.close(cb), + (cb) => rimraf(store1, cb), + (cb) => rimraf(store2, cb) + ], done) + }) + + require('./keychain.spec')(datastore1, datastore2) + require('./peerid') +}) diff --git a/test/peerid.js b/test/peerid.js new file mode 100644 index 00000000..8d3063c4 --- /dev/null +++ b/test/peerid.js @@ -0,0 +1,105 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const PeerId = require('peer-id') +const multihash = require('multihashes') +const crypto = require('libp2p-crypto') +const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') +const rsaClass = require('libp2p-crypto/src/keys/rsa-class') + +const sample = { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: '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==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=' +} + +describe('peer ID', () => { + let peer + let publicKeyDer // a buffer + + before(function (done) { + const encoded = Buffer.from(sample.privKey, 'base64') + PeerId.createFromPrivKey(encoded, (err, id) => { + peer = id + done() + }) + }) + + it('decoded public key', (done) => { + // console.log('peer id', peer.toJSON()) + // console.log('id', peer.toB58String()) + // console.log('id decoded', multihash.decode(peer.id)) + + // 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) => { + // console.log('private key', key) + // console.log('\nprivate key der', key.marshal().toString('base64')) + done() + }) + }) + + it('encoded public key with DER', (done) => { + const jwk = rsaUtils.pkixToJwk(publicKeyDer) + // console.log('jwk', jwk) + const rsa = new rsaClass.RsaPublicKey(jwk) + // console.log('rsa', rsa) + rsa.hash((err, keyId) => { + // 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() + }) + }) + + it('encoded public key with JWT', (done) => { + const jwk = { + kty: 'RSA', + n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw', + e: 'AQAB', + alg: 'RS256', + kid: '2011-04-29' + } + // console.log('jwk', jwk) + const rsa = new rsaClass.RsaPublicKey(jwk) + // console.log('rsa', rsa) + rsa.hash((err, keyId) => { + // 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() + }) + }) + + it('decoded private key', (done) => { + // console.log('peer id', peer.toJSON()) + // console.log('id', peer.toB58String()) + // console.log('id decoded', multihash.decode(peer.id)) + + // get protobuf version of the private key + const privateKeyProtobuf = peer.marshalPrivKey() + crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { + // console.log('private key', key) + //console.log('\nprivate key der', key.marshal().toString('base64')) + done() + }) + }) + +}) From 658a4d7907fc2d9cb611359b2dfd0f6952a78075 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Wed, 6 Dec 2017 23:13:02 +1300 Subject: [PATCH 04/87] docs: install and links --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f89dcdb..90ade224 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,13 @@ ## Install + npm install --save libp2p-keychain + ### Usage + const Keychain = require('libp2p-keychain') + const FsStore = require('datastore-fs') + const datastore = new FsStore('./a-keystore') const opts = { passPhrase: 'some long easily remembered phrase' @@ -91,7 +96,7 @@ const defaultOptions = { } ``` -![key storage](../doc/private-key.png?raw=true) +![key storage](./doc/private-key.png?raw=true) ### Physical storage From 409a9990cda06a8ed9434b437a549ae0f7a14056 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 00:10:22 +1300 Subject: [PATCH 05/87] fix: linting --- src/cms.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cms.js b/src/cms.js index 2f2d9c7d..fb66c2df 100644 --- a/src/cms.js +++ b/src/cms.js @@ -10,7 +10,7 @@ class CMS { throw new Error('keystore is required') } - this.keystore = keystore; + this.keystore = keystore } createAnonymousEncryptedData (name, plain, callback) { @@ -53,7 +53,7 @@ class CMS { const self = this let cms try { - const buf = forge.util.createBuffer(cmsData.toString('binary')); + const buf = forge.util.createBuffer(cmsData.toString('binary')) const obj = forge.asn1.fromDer(buf) cms = forge.pkcs7.messageFromAsn1(obj) } catch (err) { @@ -82,7 +82,7 @@ class CMS { (cb) => self.keystore.findKeyById(r.keyId, cb), (key, cb) => self.keystore._getPrivateKey(key.name, cb) ], (err, pem) => { - if (err) return callback(err); + if (err) return callback(err) const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._()) cms.decrypt(r.recipient, privateKey) @@ -91,7 +91,6 @@ class CMS { } ) } - } module.exports = CMS From 7c44c91788d3bf1a9bd366dfd79b5201b676da4e Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 00:16:38 +1300 Subject: [PATCH 06/87] fix: more linting --- src/keychain.js | 49 +++++++++++++++++++++++++------------------ src/util.js | 22 +++++++++---------- test/browser.js | 7 ++----- test/keychain.spec.js | 15 +++++++------ test/peerid.js | 8 +++++-- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index 50a67983..f4e4b0fc 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -1,11 +1,10 @@ 'use strict' const async = require('async') -const sanitize = require("sanitize-filename") +const sanitize = require('sanitize-filename') const forge = require('node-forge') const deepmerge = require('deepmerge') -const crypto = require('crypto') -const libp2pCrypto = require('libp2p-crypto') +const crypto = require('libp2p-crypto') const util = require('./util') const CMS = require('./cms') const DS = require('interface-datastore') @@ -32,7 +31,6 @@ const defaultOptions = { function validateKeyName (name) { if (!name) return false - return name === sanitize(name.trim()) } @@ -44,8 +42,9 @@ function validateKeyName (name) { * * @param {function(Error)} callback - The caller * @param {string | Error} err - The error + * @returns {undefined} */ -function _error(callback, err) { +function _error (callback, err) { const min = 200 const max = 1000 const delay = Math.random() * (max - min) + min @@ -55,6 +54,9 @@ function _error(callback, err) { /** * Converts a key name into a datastore name. + * + * @param {string} name + * @returns {DS.Key} */ function DsName (name) { return new DS.Key('/' + name) @@ -62,8 +64,11 @@ function DsName (name) { /** * Converts a datastore name into a key name. + * + * @param {DS.Key} name - A datastore name + * @returns {string} */ -function KsName(name) { +function KsName (name) { return name.toString().slice(1) } @@ -111,7 +116,7 @@ class Keychain { this.cms = new CMS(this) } - static get options() { + static get options () { return defaultOptions } @@ -123,6 +128,7 @@ class Keychain { } const dsname = DsName(name) self.store.has(dsname, (err, exists) => { + if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists'`) switch (type.toLowerCase()) { @@ -133,14 +139,14 @@ class Keychain { forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => { if (err) return _error(callback, err) - const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()); + const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()) return self.store.put(dsname, pem, (err) => { if (err) return _error(callback, err) self._getKeyInfo(name, callback) }) }) - break; + break default: return _error(callback, `Invalid key type '${type}'`) @@ -181,13 +187,14 @@ class Keychain { } const dsname = DsName(name) self.store.has(dsname, (err, exists) => { + if (err) return _error(callback, err) if (!exists) return _error(callback, `Key '${name}' does not exist'`) self.store.delete(dsname, callback) }) } - renameKey(oldName, newName, callback) { + renameKey (oldName, newName, callback) { const self = this if (!validateKeyName(oldName) || oldName === 'self') { return _error(callback, `Invalid old key name '${oldName}'`) @@ -203,6 +210,7 @@ class Keychain { } const pem = res.toString() self.store.has(newDsname, (err, exists) => { + if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${newName}' already exists'`) const batch = self.store.batch() @@ -246,7 +254,7 @@ class Keychain { }) } - importKey(name, pem, password, callback) { + importKey (name, pem, password, callback) { const self = this if (!validateKeyName(name) || name === 'self') { return _error(callback, `Invalid key name '${name}'`) @@ -256,15 +264,16 @@ class Keychain { } const dsname = DsName(name) self.store.has(dsname, (err, exists) => { + if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists'`) try { const privateKey = forge.pki.decryptRsaPrivateKey(pem, password) if (privateKey === null) { return _error(callback, 'Cannot read the key, most likely the password is wrong') } - const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._()); + const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) return self.store.put(dsname, newpem, (err) => { - if (err) return _error(callback, err) + if (err) return _error(callback, err) this._getKeyInfo(name, callback) }) @@ -280,23 +289,25 @@ class Keychain { return _error(callback, `Invalid key name '${name}'`) } if (!peer || !peer.privKey) { - return _error(callback, 'Peer.privKey \is required') + return _error(callback, 'Peer.privKey is required') } const dsname = DsName(name) self.store.has(dsname, (err, exists) => { + if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists'`) const privateKeyProtobuf = peer.marshalPrivKey() - libp2pCrypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { + crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { + if (err) return _error(callback, err) try { const der = key.marshal() - const buf = forge.util.createBuffer(der.toString('binary')); + const buf = forge.util.createBuffer(der.toString('binary')) const obj = forge.asn1.fromDer(buf) const privateKey = forge.pki.privateKeyFromAsn1(obj) if (privateKey === null) { return _error(callback, 'Cannot read the peer private key') } - const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._()); + const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) return self.store.put(dsname, pem, (err) => { if (err) return _error(callback, err) @@ -314,9 +325,9 @@ class Keychain { * * @param {string} name * @param {function(Error, string)} callback + * @returns {undefined} */ _getPrivateKey (name, callback) { - const self = this if (!validateKeyName(name)) { return _error(callback, `Invalid key name '${name}'`) } @@ -329,7 +340,6 @@ class Keychain { } _getKeyInfo (name, callback) { - const self = this if (!validateKeyName(name)) { return _error(callback, `Invalid key name '${name}'`) } @@ -356,7 +366,6 @@ class Keychain { } }) } - } module.exports = Keychain diff --git a/src/util.js b/src/util.js index c3cd5a1f..6066c33f 100644 --- a/src/util.js +++ b/src/util.js @@ -14,7 +14,7 @@ exports.keyId = (privateKey, callback) => { try { const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) const spki = pki.publicKeyToSubjectPublicKeyInfo(publicKey) - const der = new Buffer(forge.asn1.toDer(spki).getBytes(), 'binary') + const der = Buffer.from(forge.asn1.toDer(spki).getBytes(), 'binary') const jwk = rsaUtils.pkixToJwk(der) const rsa = new rsaClass.RsaPublicKey(jwk) rsa.hash((err, kid) => { @@ -33,12 +33,12 @@ exports.certificateForKey = (privateKey, callback) => { if (err) return callback(err) const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) - const cert = pki.createCertificate(); - cert.publicKey = publicKey; - cert.serialNumber = '01'; - cert.validity.notBefore = new Date(); - cert.validity.notAfter = new Date(); - cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10); + const cert = pki.createCertificate() + cert.publicKey = publicKey + cert.serialNumber = '01' + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10) var attrs = [{ name: 'organizationName', value: 'ipfs' @@ -48,9 +48,9 @@ exports.certificateForKey = (privateKey, callback) => { }, { name: 'commonName', value: kid - }]; - cert.setSubject(attrs); - cert.setIssuer(attrs); + }] + cert.setSubject(attrs) + cert.setIssuer(attrs) cert.setExtensions([{ name: 'basicConstraints', cA: true @@ -77,7 +77,7 @@ exports.certificateForKey = (privateKey, callback) => { sslCA: true, emailCA: true, objCA: true - }]); + }]) // self-sign certificate cert.sign(privateKey) diff --git a/test/browser.js b/test/browser.js index a2633bef..4e08b137 100644 --- a/test/browser.js +++ b/test/browser.js @@ -4,12 +4,9 @@ const async = require('async') const LevelStore = require('datastore-level') -// use in the browser with level.js -const browserStore = new LevelStore('my/db/name', {db: require('level-js')}) - 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-js')}) + const datastore2 = new LevelStore('test-keystore-2', {db: require('level-js')}) before((done) => { async.series([ diff --git a/test/keychain.spec.js b/test/keychain.spec.js index cc1048cf..25cae122 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -1,3 +1,4 @@ +/* eslint max-nested-callbacks: ["error", 8] */ /* eslint-env mocha */ 'use strict' @@ -28,12 +29,12 @@ module.exports = (datastore1, datastore2) => { 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 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() + expect(() => new Keychain(null, { passPhrase: passPhrase })).to.throw() }) it('has default options', () => { @@ -112,7 +113,6 @@ module.exports = (datastore1, datastore2) => { }) }) }) - }) describe('query', () => { @@ -177,7 +177,7 @@ module.exports = (datastore1, datastore2) => { }) it('is a PKCS #7 message', (done) => { - ks.cms.readData("not CMS", (err) => { + ks.cms.readData('not CMS', (err) => { expect(err).to.exist() done() }) @@ -205,7 +205,6 @@ module.exports = (datastore1, datastore2) => { done() }) }) - }) describe('exported key', () => { @@ -243,7 +242,7 @@ module.exports = (datastore1, datastore2) => { done() }) }) - }) + }) 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==' @@ -252,6 +251,7 @@ module.exports = (datastore1, datastore2) => { before(function (done) { const encoded = Buffer.from(alicePrivKey, 'base64') PeerId.createFromPrivKey(encoded, (err, id) => { + expect(err).to.not.exist() alice = id done() }) @@ -351,6 +351,5 @@ module.exports = (datastore1, datastore2) => { }) }) }) - }) } diff --git a/test/peerid.js b/test/peerid.js index 8d3063c4..7d6588cb 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -24,6 +24,7 @@ describe('peer ID', () => { before(function (done) { const encoded = Buffer.from(sample.privKey, 'base64') PeerId.createFromPrivKey(encoded, (err, id) => { + expect(err).to.not.exist() peer = id done() }) @@ -44,6 +45,7 @@ describe('peer ID', () => { // 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() @@ -56,6 +58,7 @@ describe('peer ID', () => { 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)) @@ -78,6 +81,7 @@ describe('peer ID', () => { 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)) @@ -96,10 +100,10 @@ describe('peer ID', () => { // 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')) + // console.log('\nprivate key der', key.marshal().toString('base64')) done() }) }) - }) From 98ba68ac8245667578a83de621df4077d7d99529 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 00:24:39 +1300 Subject: [PATCH 07/87] test: needs more time to generate RSA key --- test/keychain.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 25cae122..4f526ace 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -68,7 +68,7 @@ module.exports = (datastore1, datastore2) => { describe('key', () => { it('can be an RSA key', function (done) { - this.timeout(20 * 1000) + this.timeout(50 * 1000) ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => { expect(err).to.not.exist() expect(info).exist() From 569f96342e8653e21eae101342a26dbf9791021f Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 00:51:30 +1300 Subject: [PATCH 08/87] test: temporarily disable webworker tests #3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91167499..88b066a9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "aegir build", "test": "aegir test", "test:node": "aegir test -t node", - "test:browser": "aegir test -t browser -t webworker", + "test:browser": "aegir test -t browser", "release": "aegir release", "release-minor": "aegir release --type minor", "release-major": "aegir release --type major" From 358c8c2ea1d83a60e68cc462b4383e6a60bd8bb8 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 01:06:51 +1300 Subject: [PATCH 09/87] test: disable webworker --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88b066a9..e382c95c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "lint": "aegir lint", "build": "aegir build", - "test": "aegir test", + "test": "aegir test -t node -t browser", "test:node": "aegir test -t node", "test:browser": "aegir test -t browser", "release": "aegir release", From 99780ab38ab4c7d7b7557e11b3d4495c34d2c2ba Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 01:22:35 +1300 Subject: [PATCH 10/87] chore: ci coverage Fixes #2 --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e382c95c..dba6e533 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "test:browser": "aegir test -t browser", "release": "aegir release", "release-minor": "aegir release --type minor", - "release-major": "aegir release --type major" + "release-major": "aegir release --type major", + "coverage": "aegir coverage", + "coverage-publish": "aegir-coverage publish" }, "pre-commit": [ "lint", From cfdd2f47bf5cdb83332c1fda2cd3c37d61d300df Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 7 Dec 2017 01:34:29 +1300 Subject: [PATCH 11/87] chore: publish coverage report --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dba6e533..f90524fe 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", "coverage": "aegir coverage", - "coverage-publish": "aegir-coverage publish" + "coverage-publish": "aegir coverage publish" }, "pre-commit": [ "lint", From 643bcd4eb2fc426bc97f9293a41d0377142dcd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Wed, 6 Dec 2017 13:40:12 +0100 Subject: [PATCH 12/87] Add syntax highlighting to README --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 90ade224..de4b3330 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,22 @@ ## Install - npm install --save libp2p-keychain +```sh +npm install --save libp2p-keychain +``` ### Usage - const Keychain = require('libp2p-keychain') - const FsStore = require('datastore-fs') +```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) +const datastore = new FsStore('./a-keystore') +const opts = { + passPhrase: 'some long easily remembered phrase' +} +const keychain = new Keychain(datastore, opts) +``` ## API @@ -68,7 +72,7 @@ Cryptographically protected messages 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. -``` +```js { name: 'rsa-key', id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW' @@ -82,7 +86,7 @@ The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multih A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. Its file extension is `.p8`. The default options for generating the derived encryption key are in the `dek` object -``` +```js const defaultOptions = { createIfNeeded: true, From f49e753801851758b5d2fa1240e2d1b0c7d4661f Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Fri, 8 Dec 2017 14:45:02 +1300 Subject: [PATCH 13/87] fix: return info on removed key #10 --- src/keychain.js | 9 +++++---- test/keychain.spec.js | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index f4e4b0fc..e71cfa31 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -186,11 +186,12 @@ class Keychain { return _error(callback, `Invalid key name '${name}'`) } const dsname = DsName(name) - self.store.has(dsname, (err, exists) => { + self._getKeyInfo(name, (err, keyinfo) => { if (err) return _error(callback, err) - if (!exists) return _error(callback, `Key '${name}' does not exist'`) - - self.store.delete(dsname, callback) + self.store.delete(dsname, (err) => { + if (err) return _error(callback, err) + callback(null, keyinfo) + }) }) } diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 4f526ace..d0b61bcc 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -345,8 +345,11 @@ module.exports = (datastore1, datastore2) => { }) it('can remove a known key', (done) => { - ks.removeKey(renamedRsaKeyName, (err) => { + 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() }) }) From 8305d209b2f9ee23c996a5ee70513b324f6d495c Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Fri, 8 Dec 2017 14:46:38 +1300 Subject: [PATCH 14/87] fix: error message --- src/keychain.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index e71cfa31..20421e9e 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -129,7 +129,7 @@ class Keychain { const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${name}' already exists'`) + if (exists) return _error(callback, `Key '${name}' already exists`) switch (type.toLowerCase()) { case 'rsa': @@ -212,7 +212,7 @@ class Keychain { const pem = res.toString() self.store.has(newDsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${newName}' already exists'`) + if (exists) return _error(callback, `Key '${newName}' already exists`) const batch = self.store.batch() batch.put(newDsname, pem) @@ -266,7 +266,7 @@ class Keychain { const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${name}' already exists'`) + if (exists) return _error(callback, `Key '${name}' already exists`) try { const privateKey = forge.pki.decryptRsaPrivateKey(pem, password) if (privateKey === null) { @@ -295,7 +295,7 @@ class Keychain { const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${name}' already exists'`) + if (exists) return _error(callback, `Key '${name}' already exists`) const privateKeyProtobuf = peer.marshalPrivKey() crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { From 3b8d05abb81a6eb58d63c66afc64eed78bdf8551 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sat, 9 Dec 2017 20:37:00 +1300 Subject: [PATCH 15/87] docs(keychain): add API documentation --- .travis.yml | 4 --- src/keychain.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 584f308f..af201aba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,16 +13,12 @@ matrix: script: - npm run lint - npm run test - - npm run coverage - make test before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start -after_success: - - npm run coverage-publish - addons: firefox: 'latest' apt: diff --git a/src/keychain.js b/src/keychain.js index 20421e9e..e4a61d71 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -43,6 +43,7 @@ function validateKeyName (name) { * @param {function(Error)} callback - The caller * @param {string | Error} err - The error * @returns {undefined} + * @private */ function _error (callback, err) { const min = 200 @@ -57,6 +58,7 @@ function _error (callback, err) { * * @param {string} name * @returns {DS.Key} + * @private */ function DsName (name) { return new DS.Key('/' + name) @@ -67,12 +69,31 @@ function DsName (name) { * * @param {DS.Key} name - A datastore name * @returns {string} + * @private */ function KsName (name) { return name.toString().slice(1) } +/** + * Information about a key. + * + * @typedef {Object} KeyInfo + * + * @property {string} id - The universally unique key id. + * @property {string} name - The local key name. + */ + +/** + * Key management + */ 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') @@ -116,10 +137,24 @@ class Keychain { this.cms = new CMS(this) } + /** + * 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. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ createKey (name, type, size, callback) { const self = this @@ -154,6 +189,12 @@ class Keychain { }) } + /** + * List all the keys. + * + * @param {function(Error, KeyInfo[])} callback + * @returns {undefined} + */ listKeys (callback) { const self = this const query = { @@ -170,8 +211,15 @@ class Keychain { ) } - // TODO: not very efficent. + /** + * Find a key by it's name. + * + * @param {string} id - The universally unique key identifier. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ findKeyById (id, callback) { + // TODO: not very efficent. this.listKeys((err, keys) => { if (err) return _error(callback, err) @@ -180,6 +228,13 @@ class Keychain { }) } + /** + * Remove an existing key. + * + * @param {string} name - The local key name; must already exist. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ removeKey (name, callback) { const self = this if (!validateKeyName(name) || name === 'self') { @@ -195,6 +250,14 @@ class Keychain { }) } + /** + * 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. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ renameKey (oldName, newName, callback) { const self = this if (!validateKeyName(oldName) || oldName === 'self') { @@ -225,6 +288,14 @@ class Keychain { }) } + /** + * 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 + * @param {function(Error, string)} callback + * @returns {undefined} + */ exportKey (name, password, callback) { if (!validateKeyName(name)) { return _error(callback, `Invalid key name '${name}'`) @@ -255,6 +326,15 @@ class Keychain { }) } + /** + * 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. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ importKey (name, pem, password, callback) { const self = this if (!validateKeyName(name) || name === 'self') { @@ -322,7 +402,7 @@ class Keychain { } /** - * Gets the private key as PEM encoded PKCS #8 + * Gets the private key as PEM encoded PKCS #8 string. * * @param {string} name * @param {function(Error, string)} callback From f71d3a652186c364a1fc7af518540d316d70abf5 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 10 Dec 2017 17:19:20 +1300 Subject: [PATCH 16/87] fix: maps an IPFS hash name to its forge equivalent Fixes #12 --- src/keychain.js | 22 ++++++++++++++++++++-- test/keychain.spec.js | 6 ++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index e4a61d71..7f9f5085 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -19,13 +19,26 @@ const NIST = { minIterationCount: 1000 } +/** + * Maps an IPFS hash name to its forge equivalent. + * + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @private + */ +const hashName2Forge = { + 'sha1': 'sha1', + 'sha2-256': 'sha256', + 'sha2-512': 'sha512', + +} 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: 'sha512' + hash: 'sha2-512' } } @@ -120,13 +133,18 @@ class Keychain { } this.dek = opts.dek + // Get the hashing alogorithm + const hashAlgorithm = hashName2Forge[opts.dek.hash] + if (!hashAlgorithm) + throw new Error(`dek.hash '${opts.dek.hash}' is unknown or not supported`) + // Create the derived encrypting key let dek = forge.pkcs5.pbkdf2( opts.passPhrase, opts.dek.salt, opts.dek.iterationCount, opts.dek.keyLength, - opts.dek.hash) + hashAlgorithm) dek = forge.util.bytesToHex(dek) Object.defineProperty(this, '_', { value: () => dek }) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index d0b61bcc..75a9f5fe 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -41,6 +41,12 @@ module.exports = (datastore1, datastore2) => { 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() + }) + describe('key name', () => { it('is a valid filename and non-ASCII', () => { ks.removeKey('../../nasty', (err) => { From ff4f6562483b84577d326696adaab9a186be2c56 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 10 Dec 2017 17:21:26 +1300 Subject: [PATCH 17/87] fix: lint errors --- src/keychain.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index 7f9f5085..fd3409e3 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -29,9 +29,9 @@ const NIST = { const hashName2Forge = { 'sha1': 'sha1', 'sha2-256': 'sha256', - 'sha2-512': 'sha512', - + 'sha2-512': 'sha512' } + const defaultOptions = { // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ dek: { From 06917f7aba347a910e6054c7b5683a363100d817 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 10 Dec 2017 17:37:16 +1300 Subject: [PATCH 18/87] fix: lint errors --- src/keychain.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index fd3409e3..789928b1 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -27,7 +27,7 @@ const NIST = { * @private */ const hashName2Forge = { - 'sha1': 'sha1', + sha1: 'sha1', 'sha2-256': 'sha256', 'sha2-512': 'sha512' } @@ -135,8 +135,9 @@ class Keychain { // Get the hashing alogorithm const hashAlgorithm = hashName2Forge[opts.dek.hash] - if (!hashAlgorithm) + if (!hashAlgorithm) { throw new Error(`dek.hash '${opts.dek.hash}' is unknown or not supported`) + } // Create the derived encrypting key let dek = forge.pkcs5.pbkdf2( From 2dd069b05a0cd6b84bdb4c07b944a0d024c6758f Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 10 Dec 2017 21:21:10 +1300 Subject: [PATCH 19/87] test: importing openssl keys --- test/browser.js | 1 + test/node.js | 1 + test/openssl.js | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 test/openssl.js diff --git a/test/browser.js b/test/browser.js index 4e08b137..9584a635 100644 --- a/test/browser.js +++ b/test/browser.js @@ -23,5 +23,6 @@ describe('browser', () => { }) require('./keychain.spec')(datastore1, datastore2) + require('./openssl')(datastore1) require('./peerid') }) diff --git a/test/node.js b/test/node.js index b003a7c8..634716cf 100644 --- a/test/node.js +++ b/test/node.js @@ -30,5 +30,6 @@ describe('node', () => { }) require('./keychain.spec')(datastore1, datastore2) + require('./openssl')(datastore1) require('./peerid') }) diff --git a/test/openssl.js b/test/openssl.js new file mode 100644 index 00000000..17865d7f --- /dev/null +++ b/test/openssl.js @@ -0,0 +1,154 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const Keychain = require('..') + +module.exports = (datastore1) => { + describe('interop with openssl', () => { + const passPhrase = 'this is not a secure phrase' + const keyName = 'openssl-key' + let ks + + before((done) => { + ks = new Keychain(datastore1, { passPhrase: passPhrase }) + done() + }) + + it('can read a private key', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:3072 + * -pkeyopt rsa_keygen_pubexp:65537 + */ + const pem = `-----BEGIN PRIVATE KEY----- +MIIG/wIBADANBgkqhkiG9w0BAQEFAASCBukwggblAgEAAoIBgQDp0Whyqa8KmdvK +0MsQGJEBzDAEHAZc0C6cr0rkb6Xwo+yB5kjZBRDORk0UXtYGE1pYt4JhUTmMzcWO +v2xTIsdbVMQlNtput2U8kIqS1cSTkX5HxOJtCiIzntMzuR/bGPSOexkyFQ8nCUqb +ROS7cln/ixprra2KMAKldCApN3ue2jo/JI1gyoS8sekhOASAa0ufMPpC+f70sc75 +Y53VLnGBNM43iM/2lsK+GI2a13d6rRy86CEM/ygnh/EDlyNDxo+SQmy6GmSv/lmR +xgWQE2dIfK504KIxFTOphPAQAr9AsmcNnCQLhbz7YTsBz8WcytHGQ0Z5pnBQJ9AV +CX9E6DFHetvs0CNLVw1iEO06QStzHulmNEI/3P8I1TIxViuESJxSu3pSNwG1bSJZ ++Qee24vvlz/slBzK5gZWHvdm46v7vl5z7SA+whncEtjrswd8vkJk9fI/YTUbgOC0 +HWMdc2t/LTZDZ+LUSZ/b2n5trvdJSsOKTjEfuf0wICC08pUUk8MCAwEAAQKCAYEA +ywve+DQCneIezHGk5cVvp2/6ApeTruXalJZlIxsRr3eq2uNwP4X2oirKpPX2RjBo +NMKnpnsyzuOiu+Pf3hJFrTpfWzHXXm5Eq+OZcwnQO5YNY6XGO4qhSNKT9ka9Mzbo +qRKdPrCrB+s5rryVJXKYVSInP3sDSQ2IPsYpZ6GW6Mv56PuFCpjTzElzejV7M0n5 +0bRmn+MZVMVUR54KYiaCywFgUzmr3yfs1cfcsKqMRywt2J58lRy/chTLZ6LILQMv +4V01neVJiRkTmUfIWvc1ENIFM9QJlky9AvA5ASvwTTRz8yOnxoOXE/y4OVyOePjT +cz9eumu9N5dPuUIMmsYlXmRNaeGZPD9bIgKY5zOlfhlfZSuOLNH6EHBNr6JAgfwL +pdP43sbg2SSNKpBZ0iSMvpyTpbigbe3OyhnFH/TyhcC2Wdf62S9/FRsvjlRPbakW +YhKAA2kmJoydcUDO5ccEga8b7NxCdhRiczbiU2cj70pMIuOhDlGAznyxsYbtyxaB +AoHBAPy6Cbt6y1AmuId/HYfvms6i8B+/frD1CKyn+sUDkPf81xSHV7RcNrJi1S1c +V55I0y96HulsR+GmcAW1DF3qivWkdsd/b4mVkizd/zJm3/Dm8p8QOnNTtdWvYoEB +VzfAhBGaR/xflSLxZh2WE8ZHQ3IcRCXV9ZFgJ7PMeTprBJXzl0lTptvrHyo9QK1v +obLrL/KuXWS0ql1uSnJr1vtDI5uW8WU4GDENeU5b/CJHpKpjVxlGg+7pmLknxlBl +oBnZnQKBwQDs2Ky29qZ69qnPWowKceMJ53Z6uoUeSffRZ7xuBjowpkylasEROjuL +nyAihIYB7fd7R74CnRVYLI+O2qXfNKJ8HN+TgcWv8LudkRcnZDSvoyPEJAPyZGfr +olRCXD3caqtarlZO7vXSAl09C6HcL2KZ8FuPIEsuO0Aw25nESMg9eVMaIC6s2eSU +NUt6xfZw1JC0c+f0LrGuFSjxT2Dr5WKND9ageI6afuauMuosjrrOMl2g0dMcSnVz +KrtYa7Wi1N8CgcBFnuJreUplDCWtfgEen40f+5b2yAQYr4fyOFxGxdK73jVJ/HbW +wsh2n+9mDZg9jIZQ/+1gFGpA6V7W06dSf/hD70ihcKPDXSbloUpaEikC7jxMQWY4 +uwjOkwAp1bq3Kxu21a+bAKHO/H1LDTrpVlxoJQ1I9wYtRDXrvBpxU2XyASbeFmNT +FhSByFn27Ve4OD3/NrWXtoVwM5/ioX6ZvUcj55McdTWE3ddbFNACiYX9QlyOI/TY +bhWafDCPmU9fj6kCgcEAjyQEfi9jPj2FM0RODqH1zS6OdG31tfCOTYicYQJyeKSI +/hAezwKaqi9phHMDancfcupQ89Nr6vZDbNrIFLYC3W+1z7hGeabMPNZLYAs3rE60 +dv4tRHlaNRbORazp1iTBmvRyRRI2js3O++3jzOb2eILDUyT5St+UU/LkY7R5EG4a +w1df3idx9gCftXufDWHqcqT6MqFl0QgIzo5izS68+PPxitpRlR3M3Mr4rCU20Rev +blphdF+rzAavYyj1hYuRAoHBANmxwbq+QqsJ19SmeGMvfhXj+T7fNZQFh2F0xwb2 +rMlf4Ejsnx97KpCLUkoydqAs2q0Ws9Nkx2VEVx5KfUD7fWhgbpdnEPnQkfeXv9sD +vZTuAoqInN1+vj1TME6EKR/6D4OtQygSNpecv23EuqEvyXWqRVsRt9Qd2B0H4k7h +gnjREs10u7zyqBIZH7KYVgyh27WxLr859ap8cKAH6Fb+UOPtZo3sUeeume60aebn +4pMwXeXP+LO8NIfRXV8mgrm86g== +-----END PRIVATE KEY----- +` + ks.importKey(keyName, pem, '', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + expect(key).to.have.property('name', keyName) + expect(key).to.have.property('id') + ks.removeKey(keyName, done) + }) + }) + + // TODO: net.forge can not cope with this + // Uncaught AssertionError: expected [Error: Cannot read encrypted PBE data block. Unsupported OID.] to not exist + it.skip('can read a private encrypted key (v1)', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQI2563Jugj/KkCAggABIICgPxHkKtUUE8EWevq +eX9nTjqpbsv0QoXQMhegfxDELJLU8tj6V0bWNt7QDdfQ1n6FRgnNvNGick6gyqHH +yH9qC2oXwkDFP7OrHp2NEZd7DHQLLc+L4KJ/0dzsiZ1U9no7XzQMUay9Bc918ADE +pN2/EqigWkaG4gNjkAeKWr6+BNRevDXlSvls7YDboNcTiACi5zJkthivB9g3vT1m +gPdN6Gf/mmqtBTDHeqj5QsmXYqeCyo5b26JgYsziABVZDHph4ekPUsTvudRpE9Ex +baXwdYEAZxVpSbTvQ3A5qysjSZeM9ttfRTSSwL391q7dViz4+aujpk0Vj7piH+1B +CkfO8/XudRdRlnOe+KjMidktKCsMGCIOW92IlfMvIQ/Zn1GTYj9bRXONFNJ2WPND +UmCKnL7cmworwg/weRorrGKBWIGspU+tDASOPSvIGKo6Hoxm4CN1TpDRY7DAGlgm +Y3TEbMYfpXyzkPjvAhJDt03D3J9PrTO6uM5d7YUaaTmJ2TQFQVF2Lc3Uz8lDJLs0 +ZYtfQ/4H+YY2RrX7ua7t6ArUcYXZtv0J4lRYWjwV8fGPUVc0d8xLJU0Yjf4BD7K8 +rsavHo9b5YvBUX7SgUyxAEembEOe3SjQ+gPu2U5wovcjUuC9eItEEsXGrx30BQ0E +8BtK2+hp0eMkW5/BYckJkH+Yl8ypbzRGRRIZzLgeI4JveSx/mNhewfgTr+ORPThZ +mBdkD5r+ixWF174naw53L8U9wF8kiK7pIE1N9TR4USEeovLwX6Ni/2MMDZedOfof +2f77eUdLsK19/5/lcgAAYaXauXWhy2d2r3SayFrC9woy0lh2VLKRMBjcx1oWb7dp +0uxzo5Y= +-----END ENCRYPTED PRIVATE KEY----- +` + ks.importKey(keyName, pem, 'mypassword', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + expect(key).to.have.property('name', keyName) + expect(key).to.have.property('id') + ks.removeKey(keyName, done) + }) + }) + + it('can read a private encrypted key (v2)', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-256-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIhuL894loRucCAggA +MB0GCWCGSAFlAwQBKgQQEoEtsjW3iC9/u0uGvkxX7wSCAoAsX3l6JoR2OGbT8CkY +YT3RQFqquOgItYOHw6E3tir2YrmxEAo99nxoL8pdto37KSC32eAGnfv5R1zmHHSx +0M3/y2AWiCBTX95EEzdtGC1hK3PBa/qpp/xEmcrsjYN6NXxMAkhC0hMP/HdvqMAg +ee7upvaYJsJcl8QLFNayAWr8b8cZA/RBhGEIRl59Eyj6nNtxDt3bCrfe06o1CPCV +50/fRZEwFOi/C6GYvPN6MrPZO3ALBWgopLT2yQqycTKtfxYWIdOsMBkAjKf2D6Pk +u2mqBsaP4b71jIIeT4euSJLsoJV+O39s8YHXtW8GtOqp7V5kIlnm90lZ9wzeLTZ7 +HJsD/jEdYto5J3YWm2wwEDccraffJSm7UDtJBvQdIx832kxeFCcGQjW38Zl1qqkg +iTH1PLTypxj2ZuviS2EkXVFb/kVU6leWwOt6fqWFC58UvJKeCk/6veazz3PDnTWM +92ClUqFd+CZn9VT4CIaJaAc6v5NLpPp+T9sRX9AtequPm7FyTeevY9bElfyk9gW9 +JDKgKxs6DGWDa16RL5vzwtU+G3o6w6IU+mEwa6/c+hN+pRFs/KBNLLSP9OHBx7BJ +X/32Ft+VFhJaK+lQ+f+hve7od/bgKnz4c/Vtp7Dh51DgWgCpBgb8p0vqu02vTnxD +BXtDv3h75l5PhvdWfVIzpMWRYFvPR+vJi066FjAz2sjYc0NMLSYtZWyWoIInjhoX +Dp5CQujCtw/ZSSlwde1DKEWAW4SeDZAOQNvuz0rU3eosNUJxEmh3aSrcrRtDpw+Y +mBUuWAZMpz7njBi7h+JDfmSW/GAaMwrVFC2gef5375R0TejAh+COAjItyoeYEvv8 +DQd8 +-----END ENCRYPTED PRIVATE KEY----- +` + ks.importKey(keyName, pem, 'mypassword', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + expect(key).to.have.property('name', keyName) + expect(key).to.have.property('id') + ks.removeKey(keyName, done) + }) + }) + }) +} From 1b2664a902742276ae94bd759aaa474d19d17cd6 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Mon, 11 Dec 2017 14:25:54 +1300 Subject: [PATCH 20/87] refactor: keep the key info in the store --- src/keychain.js | 164 ++++++++++++++++++++++++++---------------- test/keychain.spec.js | 10 +++ 2 files changed, 111 insertions(+), 63 deletions(-) diff --git a/src/keychain.js b/src/keychain.js index 789928b1..004174d6 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -1,6 +1,5 @@ 'use strict' -const async = require('async') const sanitize = require('sanitize-filename') const forge = require('node-forge') const deepmerge = require('deepmerge') @@ -10,7 +9,8 @@ const CMS = require('./cms') const DS = require('interface-datastore') const pull = require('pull-stream') -const keyExtension = '.p8' +const keyPrefix = '/pkcs8/' +const infoPrefix = '/info/' // NIST SP 800-132 const NIST = { @@ -74,18 +74,18 @@ function _error (callback, err) { * @private */ function DsName (name) { - return new DS.Key('/' + name) + return new DS.Key(keyPrefix + name) } /** - * Converts a datastore name into a key name. + * Converts a key name into a datastore info name. * - * @param {DS.Key} name - A datastore name - * @returns {string} + * @param {string} name + * @returns {DS.Key} * @private */ -function KsName (name) { - return name.toString().slice(1) +function DsInfoName (name) { + return new DS.Key(infoPrefix + name) } /** @@ -98,7 +98,12 @@ function KsName (name) { */ /** - * Key management + * 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 { /** @@ -112,9 +117,6 @@ class Keychain { throw new Error('store is required') } this.store = store - if (this.store.opts) { - this.store.opts.extension = keyExtension - } const opts = deepmerge(defaultOptions, options) @@ -149,9 +151,6 @@ class Keychain { dek = forge.util.bytesToHex(dek) Object.defineProperty(this, '_', { value: () => dek }) - // JS magick - this._getKeyInfo = this.findKeyByName = this._getKeyInfo.bind(this) - // Provide access to protected messages this.cms = new CMS(this) } @@ -192,12 +191,22 @@ class Keychain { } forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => { if (err) return _error(callback, err) - - const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()) - return self.store.put(dsname, pem, (err) => { + util.keyId(keypair.privateKey, (err, kid) => { if (err) return _error(callback, err) - self._getKeyInfo(name, callback) + const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()) + 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) + }) }) }) break @@ -217,28 +226,27 @@ class Keychain { listKeys (callback) { const self = this const query = { - keysOnly: true + prefix: infoPrefix } pull( self.store.query(query), pull.collect((err, res) => { if (err) return _error(callback, err) - const names = res.map(r => KsName(r.key)) - async.map(names, self._getKeyInfo, callback) + const info = res.map(r => JSON.parse(r.value)) + callback(null, info) }) ) } /** - * Find a key by it's name. + * Find a key by it's id. * * @param {string} id - The universally unique key identifier. * @param {function(Error, KeyInfo)} callback * @returns {undefined} */ findKeyById (id, callback) { - // TODO: not very efficent. this.listKeys((err, keys) => { if (err) return _error(callback, err) @@ -247,6 +255,28 @@ class Keychain { }) } + /** + * Find a key by it's name. + * + * @param {string} name - The local key name. + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ + findKeyByName (name, callback) { + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + + const dsname = DsInfoName(name) + this.store.get(dsname, (err, res) => { + if (err) { + return _error(callback, `Key '${name}' does not exist. ${err.message}`) + } + + callback(null, JSON.parse(res.toString())) + }) + } + /** * Remove an existing key. * @@ -260,9 +290,12 @@ class Keychain { return _error(callback, `Invalid key name '${name}'`) } const dsname = DsName(name) - self._getKeyInfo(name, (err, keyinfo) => { + self.findKeyByName(name, (err, keyinfo) => { if (err) return _error(callback, err) - self.store.delete(dsname, (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) }) @@ -287,6 +320,8 @@ class Keychain { } 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, `Key '${oldName}' does not exist. ${err.message}`) @@ -296,12 +331,20 @@ class Keychain { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${newName}' already exists`) - const batch = self.store.batch() - batch.put(newDsname, pem) - batch.delete(oldDsname) - batch.commit((err) => { + self.store.get(oldInfoName, (err, res) => { if (err) return _error(callback, err) - self._getKeyInfo(newName, callback) + + 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) + }) }) }) }) @@ -372,10 +415,21 @@ class Keychain { return _error(callback, 'Cannot read the key, most likely the password is wrong') } const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) - return self.store.put(dsname, newpem, (err) => { + util.keyId(privateKey, (err, kid) => { if (err) return _error(callback, err) - this._getKeyInfo(name, callback) + const keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, newpem) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + batch.commit((err) => { + if (err) return _error(callback, err) + + callback(null, keyInfo) + }) }) } catch (err) { _error(callback, err) @@ -408,10 +462,21 @@ class Keychain { return _error(callback, 'Cannot read the peer private key') } const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) - return self.store.put(dsname, pem, (err) => { + util.keyId(privateKey, (err, kid) => { if (err) return _error(callback, err) - this._getKeyInfo(name, callback) + 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) + }) }) } catch (err) { _error(callback, err) @@ -426,6 +491,7 @@ class Keychain { * @param {string} name * @param {function(Error, string)} callback * @returns {undefined} + * @private */ _getPrivateKey (name, callback) { if (!validateKeyName(name)) { @@ -438,34 +504,6 @@ class Keychain { callback(null, res.toString()) }) } - - _getKeyInfo (name, callback) { - if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) - } - - const dsname = DsName(name) - this.store.get(dsname, (err, res) => { - if (err) { - return _error(callback, `Key '${name}' does not exist. ${err.message}`) - } - const pem = res.toString() - try { - const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._()) - util.keyId(privateKey, (err, kid) => { - if (err) return _error(callback, err) - - const info = { - name: name, - id: kid - } - return callback(null, info) - }) - } catch (e) { - _error(callback, e) - } - }) - } } module.exports = Keychain diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 75a9f5fe..0883984d 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -271,6 +271,16 @@ module.exports = (datastore1, datastore2) => { done() }) }) + + it('key 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() + }) + }) }) describe('rename', () => { From ee9dbeb0119bfe0ed2500c8db8fc05a9d3fbfe9b Mon Sep 17 00:00:00 2001 From: Victor Bjelkholm Date: Thu, 14 Dec 2017 18:04:39 +0100 Subject: [PATCH 21/87] Updating CI files This commit updates all CI scripts to the latest version --- .travis.yml | 6 +++++- appveyor.yml | 27 +++++++++++++++------------ ci/Jenkinsfile | 2 ++ circle.yml | 5 +---- 4 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 ci/Jenkinsfile diff --git a/.travis.yml b/.travis.yml index af201aba..5102ee5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. sudo: false language: node_js @@ -13,12 +14,15 @@ matrix: script: - npm run lint - npm run test - - make test + - npm run coverage before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start +after_success: + - npm run coverage-publish + addons: firefox: 'latest' apt: diff --git a/appveyor.yml b/appveyor.yml index ba93339b..046bf910 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,26 +1,29 @@ +# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +version: "{build}" + environment: matrix: - nodejs_version: "6" - nodejs_version: "8" -init: - - git config --global core.autocrlf input - -# cache: -# - node_modules - -platform: - - x64 +matrix: + fast_finish: true install: - - ps: Install-Product node $env:nodejs_version $env:platform + # Install Node.js + - ps: Install-Product node $env:nodejs_version + + # Upgrade npm + - npm install -g npm + + # Output our current versions for debugging - node --version - npm --version + + # Install our package dependencies - npm install test_script: - - npm test + - npm run test:node build: off - -version: "{build}" diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 100644 index 00000000..a7da2e54 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1,2 @@ +// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +javascript() diff --git a/circle.yml b/circle.yml index d67b6ae7..00096937 100644 --- a/circle.yml +++ b/circle.yml @@ -1,11 +1,8 @@ +# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. machine: node: version: stable -test: - post: - - npm run coverage -- --upload - dependencies: pre: - google-chrome --version From 9129d20bcbb424784855f58fe71ee1f66564927b Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 17 Dec 2017 11:30:52 +1300 Subject: [PATCH 22/87] docs: correct hash name --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index de4b3330..5d90826f 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multih ### Private key storage -A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. Its file extension is `.p8`. +A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. The default options for generating the derived encryption key are in the `dek` object ```js @@ -94,8 +94,8 @@ const defaultOptions = { dek: { keyLength: 512 / 8, iterationCount: 10000, - salt: 'you should override this value with a crypto secure random number', - hash: 'sha512' + salt: 'at least 16 characters long', + hash: 'sha2-512' } } ``` From e78b2483aee5f6b5540bef598c5c75c686b50c17 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 17 Dec 2017 12:43:54 +1300 Subject: [PATCH 23/87] test: key name comparision --- test/keychain.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 0883984d..2c781e3f 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -126,7 +126,7 @@ module.exports = (datastore1, datastore2) => { ks.listKeys((err, keys) => { expect(err).to.not.exist() expect(keys).to.exist() - const mykey = keys.find((k) => k.name === rsaKeyName) + const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) expect(mykey).to.exist() done() }) From 3b7c691724f5aab64cb4b53db6e03c9b9f6c5f67 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sun, 17 Dec 2017 13:36:30 +1300 Subject: [PATCH 24/87] test(openssl): verify key id --- test/openssl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/openssl.js b/test/openssl.js index 17865d7f..4c8134dc 100644 --- a/test/openssl.js +++ b/test/openssl.js @@ -146,7 +146,7 @@ DQd8 expect(err).to.not.exist() expect(key).to.exist() expect(key).to.have.property('name', keyName) - expect(key).to.have.property('id') + expect(key).to.have.property('id', 'QmeMWBbuyw8KycYhZVxMzVHK3zLH1mp2DT84X2NApqiXgn') ks.removeKey(keyName, done) }) }) From c1627a99e74d0806e1b3303f744e980856333ac4 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 21 Dec 2017 02:43:54 +1300 Subject: [PATCH 25/87] feat: use libp2p-crypto (#18) * test: openssl interop is now the responsibility of libp2p-crypto * feat: use libp2p-crypto, not node-forge, for key management * fix: use libp2p-crypto.pbkdf, not node-forge * fix: do not ship CMS This removes all depencies on node-forge * test: update dependencies * test: remove dead code --- README.md | 7 +- package.json | 12 ++- src/cms.js | 96 --------------------- src/keychain.js | 190 ++++++++++++++++-------------------------- src/util.js | 86 ------------------- test/browser.js | 1 - test/keychain.spec.js | 65 +++------------ test/node.js | 1 - test/openssl.js | 154 ---------------------------------- 9 files changed, 90 insertions(+), 522 deletions(-) delete mode 100644 src/cms.js delete mode 100644 src/util.js delete mode 100644 test/openssl.js diff --git a/README.md b/README.md index 5d90826f..a4a0a0ac 100644 --- a/README.md +++ b/README.md @@ -85,15 +85,14 @@ The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multih A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. -The default options for generating the derived encryption key are in the `dek` object +The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function. + ```js const defaultOptions = { - createIfNeeded: true, - //See https://cryptosense.com/parameter-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, - iterationCount: 10000, + iterationCount: 1000, salt: 'at least 16 characters long', hash: 'sha2-512' } diff --git a/package.json b/package.json index f90524fe..541f3080 100644 --- a/package.json +++ b/package.json @@ -45,22 +45,20 @@ "async": "^2.6.0", "deepmerge": "^1.5.2", "interface-datastore": "~0.4.1", - "libp2p-crypto": "~0.10.3", - "multihashes": "~0.4.12", - "node-forge": "~0.7.1", + "libp2p-crypto": "~0.11.0", "pull-stream": "^3.6.1", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^12.2.0", + "aegir": "^12.3.0", "chai": "^4.1.2", "chai-string": "^1.4.0", - "datastore-fs": "^0.4.1", - "datastore-level": "^0.7.0", + "datastore-fs": "~0.4.1", + "datastore-level": "~0.7.0", "dirty-chai": "^2.0.1", "level-js": "^2.2.4", "mocha": "^4.0.1", - "peer-id": "^0.10.2", + "peer-id": "~0.10.4", "pre-commit": "^1.2.2", "rimraf": "^2.6.2" } diff --git a/src/cms.js b/src/cms.js deleted file mode 100644 index fb66c2df..00000000 --- a/src/cms.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict' - -const async = require('async') -const forge = require('node-forge') -const util = require('./util') - -class CMS { - constructor (keystore) { - if (!keystore) { - throw new Error('keystore is required') - } - - this.keystore = keystore - } - - createAnonymousEncryptedData (name, plain, callback) { - const self = this - if (!Buffer.isBuffer(plain)) { - return callback(new Error('Data is required')) - } - - self.keystore._getPrivateKey(name, (err, key) => { - if (err) { - return callback(err) - } - - try { - const privateKey = forge.pki.decryptRsaPrivateKey(key, self.keystore._()) - util.certificateForKey(privateKey, (err, certificate) => { - if (err) return callback(err) - - // 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() - callback(null, Buffer.from(der, 'binary')) - }) - } catch (err) { - callback(err) - } - }) - } - - readData (cmsData, callback) { - if (!Buffer.isBuffer(cmsData)) { - return callback(new Error('CMS data is required')) - } - - 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 callback(new Error('Invalid CMS: ' + err.message)) - } - - // Find a recipient whose key we hold. We only deal with recipient certs - // issued by ipfs (O=ipfs). - const recipients = cms.recipients - .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs')) - .filter(r => r.issuer.find(a => a.shortName === 'CN')) - .map(r => { - return { - recipient: r, - keyId: r.issuer.find(a => a.shortName === 'CN').value - } - }) - async.detect( - recipients, - (r, cb) => self.keystore.findKeyById(r.keyId, (err, info) => cb(null, !err && info)), - (err, r) => { - if (err) return callback(err) - if (!r) return callback(new Error('No key found for decryption')) - - async.waterfall([ - (cb) => self.keystore.findKeyById(r.keyId, cb), - (key, cb) => self.keystore._getPrivateKey(key.name, cb) - ], (err, pem) => { - if (err) return callback(err) - - const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._()) - cms.decrypt(r.recipient, privateKey) - async.setImmediate(() => callback(null, Buffer.from(cms.content.getBytes(), 'binary'))) - }) - } - ) - } -} - -module.exports = CMS diff --git a/src/keychain.js b/src/keychain.js index 004174d6..3d20504f 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -1,11 +1,9 @@ +/* eslint max-nested-callbacks: ["error", 5] */ 'use strict' const sanitize = require('sanitize-filename') -const forge = require('node-forge') const deepmerge = require('deepmerge') const crypto = require('libp2p-crypto') -const util = require('./util') -const CMS = require('./cms') const DS = require('interface-datastore') const pull = require('pull-stream') @@ -19,24 +17,11 @@ const NIST = { minIterationCount: 1000 } -/** - * Maps an IPFS hash name to its forge equivalent. - * - * See https://github.com/multiformats/multihash/blob/master/hashtable.csv - * - * @private - */ -const hashName2Forge = { - sha1: 'sha1', - 'sha2-256': 'sha256', - 'sha2-512': 'sha512' -} - const defaultOptions = { // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, - iterationCount: 10000, + iterationCount: 1000, salt: 'you should override this value with a crypto secure random number', hash: 'sha2-512' } @@ -133,26 +118,15 @@ class Keychain { if (opts.dek.iterationCount < NIST.minIterationCount) { throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) } - this.dek = opts.dek - - // Get the hashing alogorithm - const hashAlgorithm = hashName2Forge[opts.dek.hash] - if (!hashAlgorithm) { - throw new Error(`dek.hash '${opts.dek.hash}' is unknown or not supported`) - } // Create the derived encrypting key - let dek = forge.pkcs5.pbkdf2( + const dek = crypto.pbkdf2( opts.passPhrase, opts.dek.salt, opts.dek.iterationCount, opts.dek.keyLength, - hashAlgorithm) - dek = forge.util.bytesToHex(dek) + opts.dek.hash) Object.defineProperty(this, '_', { value: () => dek }) - - // Provide access to protected messages - this.cms = new CMS(this) } /** @@ -189,31 +163,32 @@ class Keychain { if (size < 2048) { return _error(callback, `Invalid RSA key size ${size}`) } - forge.pki.rsa.generateKeyPair({bits: size, workers: -1}, (err, keypair) => { + break + default: + break + } + + 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) - util.keyId(keypair.privateKey, (err, kid) => { + 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) - const pem = forge.pki.encryptRsaPrivateKey(keypair.privateKey, this._()) - 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) - }) + callback(null, keyInfo) }) }) - break - - default: - return _error(callback, `Invalid key type '${type}'`) - } + }) + }) }) } @@ -372,19 +347,10 @@ class Keychain { return _error(callback, `Key '${name}' does not exist. ${err.message}`) } const pem = res.toString() - try { - const options = { - algorithm: 'aes256', - count: this.dek.iterationCount, - saltSize: NIST.minSaltLength, - prfAlgorithm: 'sha512' - } - const privateKey = forge.pki.decryptRsaPrivateKey(pem, this._()) - const res = forge.pki.encryptRsaPrivateKey(privateKey, password, options) - return callback(null, res) - } catch (e) { - _error(callback, e) - } + crypto.keys.import(pem, this._(), (err, privateKey) => { + if (err) return _error(callback, err) + privateKey.export(password, callback) + }) }) } @@ -409,62 +375,12 @@ class Keychain { self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - try { - const privateKey = forge.pki.decryptRsaPrivateKey(pem, password) - if (privateKey === null) { - return _error(callback, 'Cannot read the key, most likely the password is wrong') - } - const newpem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) - util.keyId(privateKey, (err, kid) => { + crypto.keys.import(pem, password, (err, privateKey) => { + if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') + privateKey.id((err, kid) => { if (err) return _error(callback, err) - - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, newpem) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - batch.commit((err) => { + privateKey.export(this._(), (err, pem) => { if (err) return _error(callback, err) - - callback(null, keyInfo) - }) - }) - } catch (err) { - _error(callback, err) - } - }) - } - - importPeer (name, peer, callback) { - const self = this - if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) - } - if (!peer || !peer.privKey) { - return _error(callback, 'Peer.privKey is required') - } - const dsname = DsName(name) - self.store.has(dsname, (err, exists) => { - if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${name}' already exists`) - - const privateKeyProtobuf = peer.marshalPrivKey() - crypto.keys.unmarshalPrivateKey(privateKeyProtobuf, (err, key) => { - if (err) return _error(callback, err) - try { - const der = key.marshal() - const buf = forge.util.createBuffer(der.toString('binary')) - const obj = forge.asn1.fromDer(buf) - const privateKey = forge.pki.privateKeyFromAsn1(obj) - if (privateKey === null) { - return _error(callback, 'Cannot read the peer private key') - } - const pem = forge.pki.encryptRsaPrivateKey(privateKey, this._()) - util.keyId(privateKey, (err, kid) => { - if (err) return _error(callback, err) - const keyInfo = { name: name, id: kid @@ -478,9 +394,43 @@ class Keychain { callback(null, keyInfo) }) }) - } catch (err) { - _error(callback, err) - } + }) + }) + }) + } + + importPeer (name, peer, callback) { + const self = this + if (!validateKeyName(name)) { + return _error(callback, `Invalid key name '${name}'`) + } + if (!peer || !peer.privKey) { + return _error(callback, 'Peer.privKey is required') + } + + 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, `Key '${name}' 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) + }) + }) }) }) } diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 6066c33f..00000000 --- a/src/util.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict' - -const forge = require('node-forge') -const pki = forge.pki -const multihash = require('multihashes') -const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') -const rsaClass = require('libp2p-crypto/src/keys/rsa-class') - -exports = module.exports - -// Create an IPFS key id; the SHA-256 multihash of a public key. -// See https://github.com/richardschneider/ipfs-encryption/issues/16 -exports.keyId = (privateKey, callback) => { - try { - const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) - const spki = pki.publicKeyToSubjectPublicKeyInfo(publicKey) - const der = Buffer.from(forge.asn1.toDer(spki).getBytes(), 'binary') - const jwk = rsaUtils.pkixToJwk(der) - const rsa = new rsaClass.RsaPublicKey(jwk) - rsa.hash((err, kid) => { - if (err) return callback(err) - - const kids = multihash.toB58String(kid) - return callback(null, kids) - }) - } catch (err) { - callback(err) - } -} - -exports.certificateForKey = (privateKey, callback) => { - exports.keyId(privateKey, (err, kid) => { - if (err) return callback(err) - - const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) - const cert = pki.createCertificate() - cert.publicKey = publicKey - cert.serialNumber = '01' - cert.validity.notBefore = new Date() - cert.validity.notAfter = new Date() - cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10) - var attrs = [{ - name: 'organizationName', - value: 'ipfs' - }, { - shortName: 'OU', - value: 'keystore' - }, { - name: 'commonName', - value: kid - }] - cert.setSubject(attrs) - cert.setIssuer(attrs) - cert.setExtensions([{ - name: 'basicConstraints', - cA: true - }, { - name: 'keyUsage', - keyCertSign: true, - digitalSignature: true, - nonRepudiation: true, - keyEncipherment: true, - dataEncipherment: true - }, { - name: 'extKeyUsage', - serverAuth: true, - clientAuth: true, - codeSigning: true, - emailProtection: true, - timeStamping: true - }, { - name: 'nsCertType', - client: true, - server: true, - email: true, - objsign: true, - sslCA: true, - emailCA: true, - objCA: true - }]) - // self-sign certificate - cert.sign(privateKey) - - return callback(null, cert) - }) -} diff --git a/test/browser.js b/test/browser.js index 9584a635..4e08b137 100644 --- a/test/browser.js +++ b/test/browser.js @@ -23,6 +23,5 @@ describe('browser', () => { }) require('./keychain.spec')(datastore1, datastore2) - require('./openssl')(datastore1) require('./peerid') }) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 2c781e3f..aae21b17 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -16,11 +16,10 @@ module.exports = (datastore1, datastore2) => { const rsaKeyName = 'tajné jméno' const renamedRsaKeyName = 'ชื่อลับ' let rsaKeyInfo - let emptyKeystore + // let emptyKeystore let ks before((done) => { - emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) ks = new Keychain(datastore2, { passPhrase: passPhrase }) done() }) @@ -163,56 +162,6 @@ module.exports = (datastore1, datastore2) => { }) }) - describe('CMS protected data', () => { - const plainData = Buffer.from('This is a message from Alice to Bob') - let cms - - it('service is available', (done) => { - expect(ks).to.have.property('cms') - done() - }) - - it('is anonymous', (done) => { - ks.cms.createAnonymousEncryptedData(rsaKeyName, plainData, (err, msg) => { - expect(err).to.not.exist() - expect(msg).to.exist() - expect(msg).to.be.instanceOf(Buffer) - cms = msg - done() - }) - }) - - it('is a PKCS #7 message', (done) => { - ks.cms.readData('not CMS', (err) => { - expect(err).to.exist() - done() - }) - }) - - it('is a PKCS #7 binary message', (done) => { - ks.cms.readData(plainData, (err) => { - expect(err).to.exist() - done() - }) - }) - - it('cannot be read without the key', (done) => { - emptyKeystore.cms.readData(cms, (err, plain) => { - expect(err).to.exist() - done() - }) - }) - - it('can be read with the key', (done) => { - ks.cms.readData(cms, (err, plain) => { - expect(err).to.not.exist() - expect(plain).to.exist() - expect(plain.toString()).to.equal(plainData.toString()) - done() - }) - }) - }) - describe('exported key', () => { let pemKey @@ -272,7 +221,17 @@ module.exports = (datastore1, datastore2) => { }) }) - it('key exists', (done) => { + 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('key name exists', (done) => { ks.findKeyByName('alice', (err, key) => { expect(err).to.not.exist() expect(key).to.exist() diff --git a/test/node.js b/test/node.js index 634716cf..b003a7c8 100644 --- a/test/node.js +++ b/test/node.js @@ -30,6 +30,5 @@ describe('node', () => { }) require('./keychain.spec')(datastore1, datastore2) - require('./openssl')(datastore1) require('./peerid') }) diff --git a/test/openssl.js b/test/openssl.js deleted file mode 100644 index 4c8134dc..00000000 --- a/test/openssl.js +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const Keychain = require('..') - -module.exports = (datastore1) => { - describe('interop with openssl', () => { - const passPhrase = 'this is not a secure phrase' - const keyName = 'openssl-key' - let ks - - before((done) => { - ks = new Keychain(datastore1, { passPhrase: passPhrase }) - done() - }) - - it('can read a private key', (done) => { - /* - * Generated with - * openssl genpkey -algorithm RSA - * -pkeyopt rsa_keygen_bits:3072 - * -pkeyopt rsa_keygen_pubexp:65537 - */ - const pem = `-----BEGIN PRIVATE KEY----- -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 -4pMwXeXP+LO8NIfRXV8mgrm86g== ------END PRIVATE KEY----- -` - ks.importKey(keyName, pem, '', (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', keyName) - expect(key).to.have.property('id') - ks.removeKey(keyName, done) - }) - }) - - // TODO: net.forge can not cope with this - // Uncaught AssertionError: expected [Error: Cannot read encrypted PBE data block. Unsupported OID.] to not exist - it.skip('can read a private encrypted key (v1)', (done) => { - /* - * Generated with - * openssl genpkey -algorithm RSA - * -pkeyopt rsa_keygen_bits:1024 - * -pkeyopt rsa_keygen_pubexp:65537 - * -out foo.pem - * openssl pkcs8 -in foo.pem -topk8 -passout pass:mypassword - */ - const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- -MIICoTAbBgkqhkiG9w0BBQMwDgQI2563Jugj/KkCAggABIICgPxHkKtUUE8EWevq -eX9nTjqpbsv0QoXQMhegfxDELJLU8tj6V0bWNt7QDdfQ1n6FRgnNvNGick6gyqHH -yH9qC2oXwkDFP7OrHp2NEZd7DHQLLc+L4KJ/0dzsiZ1U9no7XzQMUay9Bc918ADE -pN2/EqigWkaG4gNjkAeKWr6+BNRevDXlSvls7YDboNcTiACi5zJkthivB9g3vT1m -gPdN6Gf/mmqtBTDHeqj5QsmXYqeCyo5b26JgYsziABVZDHph4ekPUsTvudRpE9Ex -baXwdYEAZxVpSbTvQ3A5qysjSZeM9ttfRTSSwL391q7dViz4+aujpk0Vj7piH+1B -CkfO8/XudRdRlnOe+KjMidktKCsMGCIOW92IlfMvIQ/Zn1GTYj9bRXONFNJ2WPND -UmCKnL7cmworwg/weRorrGKBWIGspU+tDASOPSvIGKo6Hoxm4CN1TpDRY7DAGlgm -Y3TEbMYfpXyzkPjvAhJDt03D3J9PrTO6uM5d7YUaaTmJ2TQFQVF2Lc3Uz8lDJLs0 -ZYtfQ/4H+YY2RrX7ua7t6ArUcYXZtv0J4lRYWjwV8fGPUVc0d8xLJU0Yjf4BD7K8 -rsavHo9b5YvBUX7SgUyxAEembEOe3SjQ+gPu2U5wovcjUuC9eItEEsXGrx30BQ0E -8BtK2+hp0eMkW5/BYckJkH+Yl8ypbzRGRRIZzLgeI4JveSx/mNhewfgTr+ORPThZ -mBdkD5r+ixWF174naw53L8U9wF8kiK7pIE1N9TR4USEeovLwX6Ni/2MMDZedOfof -2f77eUdLsK19/5/lcgAAYaXauXWhy2d2r3SayFrC9woy0lh2VLKRMBjcx1oWb7dp -0uxzo5Y= ------END ENCRYPTED PRIVATE KEY----- -` - ks.importKey(keyName, pem, 'mypassword', (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', keyName) - expect(key).to.have.property('id') - ks.removeKey(keyName, done) - }) - }) - - it('can read a private encrypted key (v2)', (done) => { - /* - * Generated with - * openssl genpkey -algorithm RSA - * -pkeyopt rsa_keygen_bits:1024 - * -pkeyopt rsa_keygen_pubexp:65537 - * -out foo.pem - * openssl pkcs8 -in foo.pem -topk8 -v2 aes-256-cbc -passout pass:mypassword - */ - const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- -MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIhuL894loRucCAggA -MB0GCWCGSAFlAwQBKgQQEoEtsjW3iC9/u0uGvkxX7wSCAoAsX3l6JoR2OGbT8CkY -YT3RQFqquOgItYOHw6E3tir2YrmxEAo99nxoL8pdto37KSC32eAGnfv5R1zmHHSx -0M3/y2AWiCBTX95EEzdtGC1hK3PBa/qpp/xEmcrsjYN6NXxMAkhC0hMP/HdvqMAg -ee7upvaYJsJcl8QLFNayAWr8b8cZA/RBhGEIRl59Eyj6nNtxDt3bCrfe06o1CPCV -50/fRZEwFOi/C6GYvPN6MrPZO3ALBWgopLT2yQqycTKtfxYWIdOsMBkAjKf2D6Pk -u2mqBsaP4b71jIIeT4euSJLsoJV+O39s8YHXtW8GtOqp7V5kIlnm90lZ9wzeLTZ7 -HJsD/jEdYto5J3YWm2wwEDccraffJSm7UDtJBvQdIx832kxeFCcGQjW38Zl1qqkg -iTH1PLTypxj2ZuviS2EkXVFb/kVU6leWwOt6fqWFC58UvJKeCk/6veazz3PDnTWM -92ClUqFd+CZn9VT4CIaJaAc6v5NLpPp+T9sRX9AtequPm7FyTeevY9bElfyk9gW9 -JDKgKxs6DGWDa16RL5vzwtU+G3o6w6IU+mEwa6/c+hN+pRFs/KBNLLSP9OHBx7BJ -X/32Ft+VFhJaK+lQ+f+hve7od/bgKnz4c/Vtp7Dh51DgWgCpBgb8p0vqu02vTnxD -BXtDv3h75l5PhvdWfVIzpMWRYFvPR+vJi066FjAz2sjYc0NMLSYtZWyWoIInjhoX -Dp5CQujCtw/ZSSlwde1DKEWAW4SeDZAOQNvuz0rU3eosNUJxEmh3aSrcrRtDpw+Y -mBUuWAZMpz7njBi7h+JDfmSW/GAaMwrVFC2gef5375R0TejAh+COAjItyoeYEvv8 -DQd8 ------END ENCRYPTED PRIVATE KEY----- -` - ks.importKey(keyName, pem, 'mypassword', (err, key) => { - expect(err).to.not.exist() - expect(key).to.exist() - expect(key).to.have.property('name', keyName) - expect(key).to.have.property('id', 'QmeMWBbuyw8KycYhZVxMzVHK3zLH1mp2DT84X2NApqiXgn') - ks.removeKey(keyName, done) - }) - }) - }) -} From 5343b0f2de92fd290108a4d6016988b84ff9eba0 Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 20 Dec 2017 13:50:56 +0000 Subject: [PATCH 26/87] chore: update deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 541f3080..55b3b5a1 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "async": "^2.6.0", "deepmerge": "^1.5.2", - "interface-datastore": "~0.4.1", + "interface-datastore": "~0.4.2", "libp2p-crypto": "~0.11.0", "pull-stream": "^3.6.1", "sanitize-filename": "^1.6.1" @@ -53,7 +53,7 @@ "aegir": "^12.3.0", "chai": "^4.1.2", "chai-string": "^1.4.0", - "datastore-fs": "~0.4.1", + "datastore-fs": "~0.4.2", "datastore-level": "~0.7.0", "dirty-chai": "^2.0.1", "level-js": "^2.2.4", From 21611e437d46879f3d371a2b55e3249a91612684 Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 20 Dec 2017 13:52:09 +0000 Subject: [PATCH 27/87] chore: update contributors --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 55b3b5a1..1fc3591f 100644 --- a/package.json +++ b/package.json @@ -61,5 +61,11 @@ "peer-id": "~0.10.4", "pre-commit": "^1.2.2", "rimraf": "^2.6.2" - } + }, + "contributors": [ + "David Dias ", + "Maciej Krüger ", + "Richard Schneider ", + "Victor Bjelkholm " + ] } From de15d129ddd5d7e2720ebbd070e6719d318ad304 Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 20 Dec 2017 13:52:09 +0000 Subject: [PATCH 28/87] chore: release version v0.2.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ package.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5c79f501 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ + +# 0.2.0 (2017-12-20) + + +### Bug Fixes + +* error message ([8305d20](https://github.com/libp2p/js-libp2p-keychain/commit/8305d20)) +* lint errors ([06917f7](https://github.com/libp2p/js-libp2p-keychain/commit/06917f7)) +* lint errors ([ff4f656](https://github.com/libp2p/js-libp2p-keychain/commit/ff4f656)) +* linting ([409a999](https://github.com/libp2p/js-libp2p-keychain/commit/409a999)) +* maps an IPFS hash name to its forge equivalent ([f71d3a6](https://github.com/libp2p/js-libp2p-keychain/commit/f71d3a6)), closes [#12](https://github.com/libp2p/js-libp2p-keychain/issues/12) +* more linting ([7c44c91](https://github.com/libp2p/js-libp2p-keychain/commit/7c44c91)) +* return info on removed key [#10](https://github.com/libp2p/js-libp2p-keychain/issues/10) ([f49e753](https://github.com/libp2p/js-libp2p-keychain/commit/f49e753)) + + +### Features + +* move bits from https://github.com/richardschneider/ipfs-encryption ([1a96ae8](https://github.com/libp2p/js-libp2p-keychain/commit/1a96ae8)) +* use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9)) + + + diff --git a/package.json b/package.json index 1fc3591f..f70401ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.1.0", + "version": "0.2.0", "description": "Key management and cryptographically protected messages", "main": "src/index.js", "scripts": { From 89a451c147a98be1d3fbbf4a95fc00ab853b1451 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 28 Dec 2017 21:48:32 +1300 Subject: [PATCH 29/87] feat: generate unique options for a key chain (#20) --- src/keychain.js | 12 ++++++++++++ test/keychain.spec.js | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/keychain.js b/src/keychain.js index 3d20504f..28148341 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -138,6 +138,18 @@ class Keychain { return defaultOptions } + /** + * 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 + } + /** * Create a new key. * diff --git a/test/keychain.spec.js b/test/keychain.spec.js index aae21b17..32112dc5 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -46,6 +46,13 @@ module.exports = (datastore1, datastore2) => { 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', () => { ks.removeKey('../../nasty', (err) => { From 6a84873a0a77a84b5ad00234d0ee8f688aa598b7 Mon Sep 17 00:00:00 2001 From: David Dias Date: Thu, 28 Dec 2017 08:51:26 +0000 Subject: [PATCH 31/87] chore: release version v0.2.1 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c79f501..79011d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28) + + +### Features + +* generate unique options for a key chain ([#20](https://github.com/libp2p/js-libp2p-keychain/issues/20)) ([89a451c](https://github.com/libp2p/js-libp2p-keychain/commit/89a451c)) + + + # 0.2.0 (2017-12-20) diff --git a/package.json b/package.json index f70401ae..bbc1e64f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.2.0", + "version": "0.2.1", "description": "Key management and cryptographically protected messages", "main": "src/index.js", "scripts": { From 1e276f6e94b7259773cdcca54a8d5dbe3ed37e8b Mon Sep 17 00:00:00 2001 From: David Dias Date: Sun, 28 Jan 2018 20:14:57 -0800 Subject: [PATCH 32/87] chore: update deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bbc1e64f..e3f681ab 100644 --- a/package.json +++ b/package.json @@ -43,22 +43,22 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "async": "^2.6.0", - "deepmerge": "^1.5.2", + "deepmerge": "^2.0.1", "interface-datastore": "~0.4.2", - "libp2p-crypto": "~0.11.0", + "libp2p-crypto": "~0.12.0", "pull-stream": "^3.6.1", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^12.3.0", + "aegir": "^12.4.0", "chai": "^4.1.2", "chai-string": "^1.4.0", "datastore-fs": "~0.4.2", "datastore-level": "~0.7.0", "dirty-chai": "^2.0.1", "level-js": "^2.2.4", - "mocha": "^4.0.1", - "peer-id": "~0.10.4", + "mocha": "^5.0.0", + "peer-id": "~0.10.5", "pre-commit": "^1.2.2", "rimraf": "^2.6.2" }, From 2ce44446a2472ca594133e04c2269f8c627c6ea0 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Mon, 29 Jan 2018 18:44:51 +1300 Subject: [PATCH 33/87] fix: deepmerge 2.0.1 fails in browser, stay with 1.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3f681ab..955a40a3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "async": "^2.6.0", - "deepmerge": "^2.0.1", + "deepmerge": "^1.5.2", "interface-datastore": "~0.4.2", "libp2p-crypto": "~0.12.0", "pull-stream": "^3.6.1", From acf48a8efe33f0bb4c7ae0f8f9a55ff2ab378168 Mon Sep 17 00:00:00 2001 From: David Dias Date: Sun, 28 Jan 2018 22:22:59 -0800 Subject: [PATCH 35/87] chore: release version v0.3.0 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79011d74..74e27821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +# [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29) + + +### Bug Fixes + +* deepmerge 2.0.1 fails in browser, stay with 1.5.2 ([2ce4444](https://github.com/libp2p/js-libp2p-keychain/commit/2ce4444)) + + + ## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28) diff --git a/package.json b/package.json index 955a40a3..13e0a85f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.2.1", + "version": "0.3.0", "description": "Key management and cryptographically protected messages", "main": "src/index.js", "scripts": { From 5560669fc9df1bf94374f9e0218b2042409765ce Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Mon, 29 Jan 2018 19:34:55 +1300 Subject: [PATCH 36/87] CMS - PKCS #7 (#19) CMS - PKCS #7 --- .travis.yml | 4 -- README.md | 8 ++- src/cms.js | 142 ++++++++++++++++++++++++++++++++++++++++++ src/keychain.js | 30 ++++++--- src/util.js | 70 +++++++++++++++++++++ test/browser.js | 1 + test/cms-interop.js | 73 ++++++++++++++++++++++ test/keychain.spec.js | 69 +++++++++++++++++++- test/node.js | 1 + 9 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 src/cms.js create mode 100644 src/util.js create mode 100644 test/cms-interop.js diff --git a/.travis.yml b/.travis.yml index 5102ee5f..a456ff12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,15 +14,11 @@ matrix: script: - npm run lint - npm run test - - npm run coverage before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start -after_success: - - npm run coverage-publish - addons: firefox: 'latest' apt: diff --git a/README.md b/README.md index a4a0a0ac..a3b1c33e 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ A naming service for a key Cryptographically protected messages -- `cms.createAnonymousEncryptedData (name, plain, callback)` -- `cms.readData (cmsData, callback)` +- `cms.encrypt (name, plain, callback)` +- `cms.decrypt (cmsData, callback)` ### KeyInfo @@ -105,6 +105,10 @@ const defaultOptions = { The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benifit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. +### 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-crypto/issues)! diff --git a/src/cms.js b/src/cms.js new file mode 100644 index 00000000..937063cc --- /dev/null +++ b/src/cms.js @@ -0,0 +1,142 @@ +'use strict' + +const async = require('async') +const forge = require('node-forge') +const util = require('./util') + +/** + * Cryptographic Message Syntax (aka PKCS #7) + * + * CMS describes an encapsulation syntax for data protection. It + * is used to digitally sign, digest, authenticate, or encrypt + * arbitrary message content. + * + * See RFC 5652 for all the details. + */ +class CMS { + /** + * Creates a new instance with a keychain + * + * @param {Keychain} keychain - the available keys + */ + constructor (keychain) { + if (!keychain) { + throw new Error('keychain is required') + } + + this.keychain = keychain + } + + /** + * Creates some protected data. + * + * The output Buffer contains the PKCS #7 message in DER. + * + * @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) => async.setImmediate(() => callback(err, result)) + + if (!Buffer.isBuffer(plain)) { + return done(new Error('Plain data must be a Buffer')) + } + + async.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) + + // 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) + } + }) + } + + /** + * Reads some protected data. + * + * 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'. 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) => async.setImmediate(() => callback(err, result)) + + if (!Buffer.isBuffer(cmsData)) { + return done(new Error('CMS data is required')) + } + + 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(new Error('Invalid CMS: ' + err.message)) + } + + // Find a recipient whose key we hold. We only deal with recipient certs + // issued by ipfs (O=ipfs). + const recipients = cms.recipients + .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs')) + .filter(r => r.issuer.find(a => a.shortName === 'CN')) + .map(r => { + return { + recipient: r, + keyId: r.issuer.find(a => a.shortName === 'CN').value + } + }) + async.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 = new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')) + err.missingKeys = missingKeys + return done(err) + } + + async.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')) + }) + } + ) + } +} + +module.exports = CMS diff --git a/src/keychain.js b/src/keychain.js index 28148341..41f5c1c4 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -6,6 +6,7 @@ const deepmerge = require('deepmerge') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') const pull = require('pull-stream') +const CMS = require('./cms') const keyPrefix = '/pkcs8/' const infoPrefix = '/info/' @@ -21,7 +22,7 @@ const defaultOptions = { // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, - iterationCount: 1000, + iterationCount: 10000, salt: 'you should override this value with a crypto secure random number', hash: 'sha2-512' } @@ -86,8 +87,8 @@ function DsInfoName (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 + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key * */ class Keychain { @@ -130,12 +131,17 @@ class Keychain { } /** - * The default options for a keychain. + * Gets an object that can encrypt/decrypt protected data + * using the Cryptographic Message Syntax (CMS). * - * @returns {object} + * CMS describes an encapsulation syntax for data protection. It + * is used to digitally sign, digest, authenticate, or encrypt + * arbitrary message content. + * + * @returns {CMS} */ - static get options () { - return defaultOptions + get cms () { + return new CMS(this) } /** @@ -150,6 +156,16 @@ class Keychain { 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. * diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..9aa248ff --- /dev/null +++ b/src/util.js @@ -0,0 +1,70 @@ +'use strict' + +const forge = require('node-forge') +const pki = forge.pki +exports = module.exports + +/** + * Gets a self-signed X.509 certificate for the key. + * + * The output Buffer contains the PKCS #7 message in DER. + * + * TODO: move to libp2p-crypto package + * + * @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) => { + const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e) + const cert = pki.createCertificate() + cert.publicKey = publicKey + cert.serialNumber = '01' + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10) + const attrs = [{ + name: 'organizationName', + value: 'ipfs' + }, { + shortName: 'OU', + value: 'keystore' + }, { + name: 'commonName', + value: key.id + }] + cert.setSubject(attrs) + cert.setIssuer(attrs) + cert.setExtensions([{ + name: 'basicConstraints', + cA: true + }, { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + emailProtection: true, + timeStamping: true + }, { + name: 'nsCertType', + client: true, + server: true, + email: true, + objsign: true, + sslCA: true, + emailCA: true, + objCA: true + }]) + // self-sign certificate + cert.sign(privateKey) + + return callback(null, cert) +} diff --git a/test/browser.js b/test/browser.js index 4e08b137..e1aa2b00 100644 --- a/test/browser.js +++ b/test/browser.js @@ -23,5 +23,6 @@ describe('browser', () => { }) require('./keychain.spec')(datastore1, datastore2) + require('./cms-interop')(datastore2) require('./peerid') }) diff --git a/test/cms-interop.js b/test/cms-interop.js new file mode 100644 index 00000000..a7449984 --- /dev/null +++ b/test/cms-interop.js @@ -0,0 +1,73 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +chai.use(require('chai-string')) +const Keychain = require('..') + +module.exports = (datastore) => { + describe('cms interop', () => { + const passPhrase = 'this is not a secure phrase' + const aliceKeyName = 'cms-interop-alice' + let ks + + before((done) => { + 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) { + this.timeout(10 * 1000) + const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA' + const alice = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA +MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG +QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd +1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7 +/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A +CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri +dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA +ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY +zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/ +ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt +0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83 +GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH +igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m +3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE +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() + }) + }) + + it('decrypts node-forge example', (done) => { + const example = ` +MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK +EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI +WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B +AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k +d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO +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() + }) + }) + }) +} diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 32112dc5..ae78cb1e 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -16,11 +16,12 @@ module.exports = (datastore1, datastore2) => { const rsaKeyName = 'tajné jméno' const renamedRsaKeyName = 'ชื่อลับ' let rsaKeyInfo - // let emptyKeystore + let emptyKeystore let ks before((done) => { ks = new Keychain(datastore2, { passPhrase: passPhrase }) + emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase }) done() }) @@ -169,6 +170,72 @@ module.exports = (datastore1, datastore2) => { }) }) + describe('CMS protected data', () => { + const plainData = Buffer.from('This is a message from Alice to Bob') + let cms + + it('service is available', (done) => { + 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 plain data as a Buffer', (done) => { + ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => { + expect(err).to.exist() + done() + }) + }) + + 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('is a PKCS #7 message', (done) => { + ks.cms.decrypt('not CMS', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('is a PKCS #7 binary message', (done) => { + ks.cms.decrypt(plainData, (err) => { + expect(err).to.exist() + done() + }) + }) + + 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]) + done() + }) + }) + + 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() + }) + }) + }) + describe('exported key', () => { let pemKey diff --git a/test/node.js b/test/node.js index b003a7c8..6ca293ee 100644 --- a/test/node.js +++ b/test/node.js @@ -30,5 +30,6 @@ describe('node', () => { }) require('./keychain.spec')(datastore1, datastore2) + require('./cms-interop')(datastore2) require('./peerid') }) From ee978a54ea83bdd47be9e2c7d48f0ce403cd9c61 Mon Sep 17 00:00:00 2001 From: David Dias Date: Sun, 28 Jan 2018 22:36:45 -0800 Subject: [PATCH 38/87] chore: release version v0.3.1 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e27821..06b1412f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29) + + + # [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29) diff --git a/package.json b/package.json index 13e0a85f..f04b5c8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.0", + "version": "0.3.1", "description": "Key management and cryptographically protected messages", "main": "src/index.js", "scripts": { From 974c5070696cf1233f9e687a4ca806677332ccd0 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 25 Jun 2018 15:06:17 +0200 Subject: [PATCH 39/87] docs: add lead-maintainer * docs: add lead-maintainer --- README.md | 4 ++++ package.json | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a3b1c33e..24bcf0d6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ > A secure key chain for libp2p in JavaScript +## Lead Maintainer + +[Vasco Santos](https://github.com/vasco-santos). + ## Features - Manages the lifecycle of a key diff --git a/package.json b/package.json index f04b5c8e..73598b9f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "libp2p-keychain", "version": "0.3.1", "description": "Key management and cryptographically protected messages", + "leadMaintainer": "Vasco Santos ", "main": "src/index.js", "scripts": { "lint": "aegir lint", @@ -15,7 +16,7 @@ "coverage": "aegir coverage", "coverage-publish": "aegir coverage publish" }, - "pre-commit": [ + "pre-push": [ "lint", "test" ], @@ -35,7 +36,6 @@ "secure", "crypto" ], - "author": "Richard Schneider ", "license": "MIT", "bugs": { "url": "https://github.com/libp2p/js-libp2p-keychain/issues" @@ -59,7 +59,6 @@ "level-js": "^2.2.4", "mocha": "^5.0.0", "peer-id": "~0.10.5", - "pre-commit": "^1.2.2", "rimraf": "^2.6.2" }, "contributors": [ From 0065b0a49e1b5403cda2ef77d4d9a94f00c01c73 Mon Sep 17 00:00:00 2001 From: Masahiro Saito Date: Sat, 30 Jun 2018 07:38:19 +0900 Subject: [PATCH 40/87] chore: fix out of date npms (#21) --- package.json | 6 +++--- src/keychain.js | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 73598b9f..6526a147 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "async": "^2.6.0", - "deepmerge": "^1.5.2", "interface-datastore": "~0.4.2", - "libp2p-crypto": "~0.12.0", + "libp2p-crypto": "~0.13.0", + "lodash.merge": "^4.6.1", "pull-stream": "^3.6.1", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^12.4.0", + "aegir": "^13.0.7", "chai": "^4.1.2", "chai-string": "^1.4.0", "datastore-fs": "~0.4.2", diff --git a/src/keychain.js b/src/keychain.js index 41f5c1c4..0ec392a6 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -2,7 +2,7 @@ 'use strict' const sanitize = require('sanitize-filename') -const deepmerge = require('deepmerge') +const deepmerge = require('lodash.merge') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') const pull = require('pull-stream') @@ -104,7 +104,8 @@ class Keychain { } this.store = store - const opts = deepmerge(defaultOptions, options) + const opts = {} + deepmerge(opts, defaultOptions, options) // Enforce NIST SP 800-132 if (!opts.passPhrase || opts.passPhrase.length < 20) { From 73d4530c5bc00e082ee2398945e195ce7d781cfe Mon Sep 17 00:00:00 2001 From: David Dias Date: Sat, 30 Jun 2018 15:13:49 +0100 Subject: [PATCH 41/87] chore: update deps --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 6526a147..d09ff45d 100644 --- a/package.json +++ b/package.json @@ -42,23 +42,23 @@ }, "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { - "async": "^2.6.0", + "async": "^2.6.1", "interface-datastore": "~0.4.2", "libp2p-crypto": "~0.13.0", "lodash.merge": "^4.6.1", - "pull-stream": "^3.6.1", + "pull-stream": "^3.6.8", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^13.0.7", + "aegir": "^14.0.0", "chai": "^4.1.2", "chai-string": "^1.4.0", - "datastore-fs": "~0.4.2", - "datastore-level": "~0.7.0", + "datastore-fs": "~0.5.0", + "datastore-level": "~0.8.0", "dirty-chai": "^2.0.1", - "level-js": "^2.2.4", - "mocha": "^5.0.0", - "peer-id": "~0.10.5", + "level-js": "^3.0.0", + "mocha": "^5.2.0", + "peer-id": "~0.10.7", "rimraf": "^2.6.2" }, "contributors": [ From f95fef4ad2e8bdf1d132c5264d8078bc5cf090f0 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 3 Jul 2018 16:21:32 +0200 Subject: [PATCH 42/87] chore: use lodash main dependency --- package.json | 2 +- src/keychain.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d09ff45d..62155f30 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "async": "^2.6.1", "interface-datastore": "~0.4.2", "libp2p-crypto": "~0.13.0", - "lodash.merge": "^4.6.1", + "lodash": "^4.6.1", "pull-stream": "^3.6.8", "sanitize-filename": "^1.6.1" }, diff --git a/src/keychain.js b/src/keychain.js index 0ec392a6..da94ab2e 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -2,7 +2,7 @@ 'use strict' const sanitize = require('sanitize-filename') -const deepmerge = require('lodash.merge') +const deepmerge = require('lodash/merge') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') const pull = require('pull-stream') From 8dfaab1af03bcadf7bad60d903180fcebb189e13 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 18 Sep 2018 12:48:58 +0100 Subject: [PATCH 43/87] fix: validate createKey params properly (#26) License: MIT Signed-off-by: Alan Shaw --- src/keychain.js | 12 ++++++++++++ test/keychain.spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/keychain.js b/src/keychain.js index da94ab2e..a9619fdb 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -6,6 +6,8 @@ const deepmerge = require('lodash/merge') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') const pull = require('pull-stream') +const isString = require('lodash/isString') +const isSafeInteger = require('lodash/isSafeInteger') const CMS = require('./cms') const keyPrefix = '/pkcs8/' @@ -30,6 +32,7 @@ const defaultOptions = { function validateKeyName (name) { if (!name) return false + if (!isString(name)) return false return name === sanitize(name.trim()) } @@ -182,6 +185,15 @@ class Keychain { if (!validateKeyName(name) || name === 'self') { return _error(callback, `Invalid key name '${name}'`) } + + if (!isString(type)) { + return _error(callback, `Invalid key type '${type}'`) + } + + if (!isSafeInteger(size)) { + return _error(callback, `Invalid key size '${size}'`) + } + const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index ae78cb1e..ed6f1a80 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -117,6 +117,30 @@ module.exports = (datastore1, datastore2) => { }) }) + 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('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('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() + }) + }) + describe('implements NIST SP 800-131A', () => { it('disallows RSA length < 2048', (done) => { ks.createKey('bad-nist-rsa', 'rsa', 1024, (err) => { From 65129bff3b445a8df0cfd390635c7f022dbf6c11 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 18 Sep 2018 13:02:43 +0100 Subject: [PATCH 44/87] chore: update contributors --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 62155f30..77fdec15 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,12 @@ "rimraf": "^2.6.2" }, "contributors": [ + "Alan Shaw ", "David Dias ", "Maciej Krüger ", + "Masahiro Saito ", "Richard Schneider ", + "Vasco Santos ", "Victor Bjelkholm " ] } From 5d3f489f23622415cb6d29d891d87743f295b06a Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 18 Sep 2018 13:02:43 +0100 Subject: [PATCH 45/87] chore: release version v0.3.2 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b1412f..8714d39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18) + + +### Bug Fixes + +* validate createKey params properly ([#26](https://github.com/libp2p/js-libp2p-keychain/issues/26)) ([8dfaab1](https://github.com/libp2p/js-libp2p-keychain/commit/8dfaab1)) + + + ## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29) diff --git a/package.json b/package.json index 77fdec15..958a6f46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.1", + "version": "0.3.2", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From 24d4374b2094918e556c11f2d2f0c2032ad9b98d Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 25 Oct 2018 09:31:32 +0100 Subject: [PATCH 46/87] chore: upgrade dependencies (#27) --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 958a6f46..3f091552 100644 --- a/package.json +++ b/package.json @@ -43,15 +43,15 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "async": "^2.6.1", - "interface-datastore": "~0.4.2", + "interface-datastore": "~0.6.0", "libp2p-crypto": "~0.13.0", "lodash": "^4.6.1", "pull-stream": "^3.6.8", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^14.0.0", - "chai": "^4.1.2", + "aegir": "^15.3.0", + "chai": "^4.2.0", "chai-string": "^1.4.0", "datastore-fs": "~0.5.0", "datastore-level": "~0.8.0", From 571c81a2be5a09bdbb58e6b8699fdb415b82b645 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 25 Oct 2018 09:37:22 +0100 Subject: [PATCH 47/87] chore: update contributors --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f091552..e9e265e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.2", + "version": "0.3.3", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -68,6 +68,7 @@ "Masahiro Saito ", "Richard Schneider ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From 251e0b87b64626798405b6c0da2c8246b141b9b5 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 25 Oct 2018 09:37:22 +0100 Subject: [PATCH 48/87] chore: release version v0.3.3 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8714d39d..1c9548a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25) + + + ## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18) From 17268d5fe3bdd5e92f0fabb5a39c39841dd72a79 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 4 Jan 2019 10:51:56 +0000 Subject: [PATCH 49/87] chore: update dependencies (#29) --- .travis.yml | 28 ---------------------------- circle.yml | 15 --------------- package.json | 20 ++++++++++---------- test/browser.js | 4 ++-- 4 files changed, 12 insertions(+), 55 deletions(-) delete mode 100644 .travis.yml delete mode 100644 circle.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a456ff12..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. -sudo: false -language: node_js - -matrix: - include: - - node_js: 6 - env: CXX=g++-4.8 - - node_js: 8 - env: CXX=g++-4.8 - # - node_js: stable - # env: CXX=g++-4.8 - -script: - - npm run lint - - npm run test - -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - -addons: - firefox: 'latest' - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 00096937..00000000 --- a/circle.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. -machine: - node: - version: stable - -dependencies: - pre: - - google-chrome --version - - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - - sudo dpkg -i google-chrome.deb || true - - sudo apt-get update - - sudo apt-get install -f - - sudo apt-get install --only-upgrade lsb-base - - sudo dpkg -i google-chrome.deb - - google-chrome --version diff --git a/package.json b/package.json index e9e265e2..15d8f4ba 100644 --- a/package.json +++ b/package.json @@ -44,22 +44,22 @@ "dependencies": { "async": "^2.6.1", "interface-datastore": "~0.6.0", - "libp2p-crypto": "~0.13.0", - "lodash": "^4.6.1", - "pull-stream": "^3.6.8", + "libp2p-crypto": "~0.15.0", + "lodash": "^4.17.11", + "pull-stream": "^3.6.9", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^15.3.0", + "aegir": "^18.0.2", "chai": "^4.2.0", - "chai-string": "^1.4.0", - "datastore-fs": "~0.5.0", - "datastore-level": "~0.8.0", + "chai-string": "^1.5.0", + "datastore-fs": "~0.7.0", + "datastore-level": "~0.10.0", "dirty-chai": "^2.0.1", - "level-js": "^3.0.0", + "level-js": "^4.0.0", "mocha": "^5.2.0", - "peer-id": "~0.10.7", - "rimraf": "^2.6.2" + "peer-id": "~0.12.1", + "rimraf": "^2.6.3" }, "contributors": [ "Alan Shaw ", diff --git a/test/browser.js b/test/browser.js index e1aa2b00..374ce6d0 100644 --- a/test/browser.js +++ b/test/browser.js @@ -5,8 +5,8 @@ const async = require('async') 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-js') }) + const datastore2 = new LevelStore('test-keystore-2', { db: require('level-js') }) before((done) => { async.series([ From a753b1c88276f7753adfe3b2e1ffe68ba88b0133 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 4 Jan 2019 10:54:24 +0000 Subject: [PATCH 50/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15d8f4ba..9cd9fed0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.3", + "version": "0.3.4", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From 4b895cf46ffdf0eae8ae15fee1bb4800d6f97e9b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 4 Jan 2019 10:54:24 +0000 Subject: [PATCH 51/87] chore: release version v0.3.4 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9548a9..01ba3a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04) + + + ## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25) From 7eeed87b101a6a68560f350502f230e38bd276b6 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Thu, 10 Jan 2019 11:16:03 +0000 Subject: [PATCH 52/87] fix: reduce bundle size (#28) --- .gitignore | 1 + appveyor.yml | 29 ----------------------------- package.json | 14 ++++++++------ src/cms.js | 19 ++++++++++++------- src/keychain.js | 18 ++++++++---------- src/util.js | 3 ++- test/browser.js | 6 +++--- test/node.js | 6 +++--- 8 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 appveyor.yml diff --git a/.gitignore b/.gitignore index 1c73b378..b64f0852 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ test/test-data/go-ipfs-repo/LOG.old # while testing npm5 package-lock.json +yarn.lock \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 046bf910..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. -version: "{build}" - -environment: - matrix: - - nodejs_version: "6" - - nodejs_version: "8" - -matrix: - fast_finish: true - -install: - # Install Node.js - - ps: Install-Product node $env:nodejs_version - - # Upgrade npm - - npm install -g npm - - # Output our current versions for debugging - - node --version - - npm --version - - # Install our package dependencies - - npm install - -test_script: - - npm run test:node - -build: off diff --git a/package.json b/package.json index 9cd9fed0..3b975289 100644 --- a/package.json +++ b/package.json @@ -44,21 +44,23 @@ "dependencies": { "async": "^2.6.1", "interface-datastore": "~0.6.0", - "libp2p-crypto": "~0.15.0", - "lodash": "^4.17.11", - "pull-stream": "^3.6.9", + "libp2p-crypto": "~0.16.0", + "merge-options": "^1.0.1", + "node-forge": "~0.7.6", + "pull-stream": "^3.6.8", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^18.0.2", + "aegir": "^18.0.3", "chai": "^4.2.0", - "chai-string": "^1.5.0", + "chai-string": "^1.4.0", "datastore-fs": "~0.7.0", "datastore-level": "~0.10.0", "dirty-chai": "^2.0.1", "level-js": "^4.0.0", "mocha": "^5.2.0", - "peer-id": "~0.12.1", + "multihashes": "~0.4.14", + "peer-id": "~0.12.2", "rimraf": "^2.6.3" }, "contributors": [ diff --git a/src/cms.js b/src/cms.js index 937063cc..90d7d85f 100644 --- a/src/cms.js +++ b/src/cms.js @@ -1,7 +1,12 @@ 'use strict' -const async = require('async') -const forge = require('node-forge') +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') /** @@ -39,13 +44,13 @@ class CMS { */ encrypt (name, plain, callback) { const self = this - const done = (err, result) => async.setImmediate(() => callback(err, result)) + const done = (err, result) => setImmediate(() => callback(err, result)) if (!Buffer.isBuffer(plain)) { return done(new Error('Plain data must be a Buffer')) } - async.series([ + series([ (cb) => self.keychain.findKeyByName(name, cb), (cb) => self.keychain._getPrivateKey(name, cb) ], (err, results) => { @@ -85,7 +90,7 @@ class CMS { * @returns {undefined} */ decrypt (cmsData, callback) { - const done = (err, result) => async.setImmediate(() => callback(err, result)) + const done = (err, result) => setImmediate(() => callback(err, result)) if (!Buffer.isBuffer(cmsData)) { return done(new Error('CMS data is required')) @@ -112,7 +117,7 @@ class CMS { keyId: r.issuer.find(a => a.shortName === 'CN').value } }) - async.detect( + detect( recipients, (r, cb) => self.keychain.findKeyById(r.keyId, (err, info) => cb(null, !err && info)), (err, r) => { @@ -124,7 +129,7 @@ class CMS { return done(err) } - async.waterfall([ + waterfall([ (cb) => self.keychain.findKeyById(r.keyId, cb), (key, cb) => self.keychain._getPrivateKey(key.name, cb) ], (err, pem) => { diff --git a/src/keychain.js b/src/keychain.js index a9619fdb..cecc3207 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -2,12 +2,11 @@ 'use strict' const sanitize = require('sanitize-filename') -const deepmerge = require('lodash/merge') +const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') -const pull = require('pull-stream') -const isString = require('lodash/isString') -const isSafeInteger = require('lodash/isSafeInteger') +const collect = require('pull-stream/sinks/collect') +const pull = require('pull-stream/pull') const CMS = require('./cms') const keyPrefix = '/pkcs8/' @@ -32,7 +31,7 @@ const defaultOptions = { function validateKeyName (name) { if (!name) return false - if (!isString(name)) return false + if (typeof name !== 'string') return false return name === sanitize(name.trim()) } @@ -107,8 +106,7 @@ class Keychain { } this.store = store - const opts = {} - deepmerge(opts, defaultOptions, options) + const opts = mergeOptions(defaultOptions, options) // Enforce NIST SP 800-132 if (!opts.passPhrase || opts.passPhrase.length < 20) { @@ -186,11 +184,11 @@ class Keychain { return _error(callback, `Invalid key name '${name}'`) } - if (!isString(type)) { + if (typeof type !== 'string') { return _error(callback, `Invalid key type '${type}'`) } - if (!isSafeInteger(size)) { + if (!Number.isSafeInteger(size)) { return _error(callback, `Invalid key size '${size}'`) } @@ -246,7 +244,7 @@ class Keychain { } pull( self.store.query(query), - pull.collect((err, res) => { + collect((err, res) => { if (err) return _error(callback, err) const info = res.map(r => JSON.parse(r.value)) diff --git a/src/util.js b/src/util.js index 9aa248ff..bc61c5b7 100644 --- a/src/util.js +++ b/src/util.js @@ -1,6 +1,7 @@ 'use strict' -const forge = require('node-forge') +require('node-forge/lib/x509') +const forge = require('node-forge/lib/forge') const pki = forge.pki exports = module.exports diff --git a/test/browser.js b/test/browser.js index 374ce6d0..0a37bedd 100644 --- a/test/browser.js +++ b/test/browser.js @@ -1,7 +1,7 @@ /* eslint-env mocha */ 'use strict' -const async = require('async') +const series = require('async/series') const LevelStore = require('datastore-level') describe('browser', () => { @@ -9,14 +9,14 @@ describe('browser', () => { const datastore2 = new LevelStore('test-keystore-2', { db: require('level-js') }) before((done) => { - async.series([ + series([ (cb) => datastore1.open(cb), (cb) => datastore2.open(cb) ], done) }) after((done) => { - async.series([ + series([ (cb) => datastore1.close(cb), (cb) => datastore2.close(cb) ], done) diff --git a/test/node.js b/test/node.js index 6ca293ee..e11d0744 100644 --- a/test/node.js +++ b/test/node.js @@ -4,7 +4,7 @@ const os = require('os') const path = require('path') const rimraf = require('rimraf') -const async = require('async') +const series = require('async/series') const FsStore = require('datastore-fs') describe('node', () => { @@ -14,14 +14,14 @@ describe('node', () => { const datastore2 = new FsStore(store2) before((done) => { - async.series([ + series([ (cb) => datastore1.open(cb), (cb) => datastore2.open(cb) ], done) }) after((done) => { - async.series([ + series([ (cb) => datastore1.close(cb), (cb) => datastore2.close(cb), (cb) => rimraf(store1, cb), From 5cbded55d516750da1b6069c85aa39d1de6aaa75 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 10 Jan 2019 11:24:15 +0000 Subject: [PATCH 53/87] chore: update contributors --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9cd9fed0..069fe75c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.4", + "version": "0.3.5", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -67,8 +67,8 @@ "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", - "Vasco Santos ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From 4dd2ad36dd03e66a35d7bc40b9a463bd19a76bf9 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 10 Jan 2019 11:24:15 +0000 Subject: [PATCH 54/87] chore: release version v0.3.5 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ba3a4b..21b3e53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10) + + + ## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04) From eaf6a88b47f08cec4bff147b110725b6957298d3 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 10 Jan 2019 11:33:28 +0000 Subject: [PATCH 55/87] chore: update contributors --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fb4d11b9..9055b7cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.5", + "version": "0.3.6", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -66,6 +66,7 @@ "contributors": [ "Alan Shaw ", "David Dias ", + "Hugo Dias ", "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", From aa5a6cb73c564a0b01dfb6d2b74c4ca490b8e2cd Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 10 Jan 2019 11:33:28 +0000 Subject: [PATCH 56/87] chore: release version v0.3.6 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b3e53c..5f9af609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10) + + +### Bug Fixes + +* reduce bundle size ([#28](https://github.com/libp2p/js-libp2p-keychain/issues/28)) ([7eeed87](https://github.com/libp2p/js-libp2p-keychain/commit/7eeed87)) + + + ## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10) From 3779bd0ba2e579432e55c1d01ca3d5f1daabed13 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 18 Feb 2019 14:15:55 +0000 Subject: [PATCH 57/87] chore: use travis (#32) --- .travis.yml | 42 ++++++++++++++++++++++++++++++++++++++++++ ci/Jenkinsfile | 2 -- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 .travis.yml delete mode 100644 ci/Jenkinsfile diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..37005a25 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +language: node_js +cache: npm +stages: + - check + - test + - cov + +node_js: + - '10' + +os: + - linux + - osx + - windows + +script: npx nyc -s npm run test:node -- --bail +after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov + +jobs: + include: + - stage: check + script: + - npx aegir commitlint --travis + - npx aegir dep-check -- -i wrtc -i electron-webrtc + - npm run lint + + - stage: test + name: chrome + addons: + chrome: stable + script: + - npx aegir test -t browser + + - stage: test + name: firefox + addons: + firefox: latest + script: + - npx aegir test -t browser -- --browsers FirefoxHeadless + +notifications: + email: false diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile deleted file mode 100644 index a7da2e54..00000000 --- a/ci/Jenkinsfile +++ /dev/null @@ -1,2 +0,0 @@ -// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. -javascript() From 9eb11f42452b4266db0c19de3310e809a0087669 Mon Sep 17 00:00:00 2001 From: Alberto Elias Date: Mon, 25 Feb 2019 12:04:54 +0100 Subject: [PATCH 58/87] feat: adds support for ed25199 and secp256k1 (#31) --- README.md | 6 +- src/keychain.js | 130 +++++++++++++++++++++------------------- test/keychain.spec.js | 134 ++++++++++++++++++++++++++++++------------ 3 files changed, 170 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 24bcf0d6..655ff03d 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Managing a key - `createKey (name, type, size, callback)` - `renameKey (oldName, newName, callback)` - `removeKey (name, callback)` -- `exportKey (name, password, callback)` -- `importKey (name, pem, password, callback)` +- `exportKey (name, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys +- `importKey (name, encKey, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys - `importPeer (name, peer, callback)` A naming service for a key @@ -67,7 +67,7 @@ A naming service for a key - `findKeyById (id, callback)` - `findKeyByName (name, callback)` -Cryptographically protected messages +Cryptographically protected messages (Only supported with RSA keys) - `cms.encrypt (name, plain, callback)` - `cms.decrypt (cmsData, callback)` diff --git a/src/keychain.js b/src/keychain.js index cecc3207..e236d8a8 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -20,7 +20,7 @@ const NIST = { } const defaultOptions = { - // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + // See https://cryptosense.com/blog/parameter-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, iterationCount: 10000, @@ -197,7 +197,8 @@ class Keychain { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - switch (type.toLowerCase()) { + type = type.toLowerCase() + switch (type) { case 'rsa': if (size < 2048) { return _error(callback, `Invalid RSA key size ${size}`) @@ -211,21 +212,16 @@ class Keychain { 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) + if (type === 'ed25519' || type === 'secp256k1') { + const keypairMarshal = keypair.bytes + self._storeKey(name, kid, keypairMarshal, dsname, callback) + } else { + keypair.export(this._(), (err, pem) => { + if (err) return _error(callback, err) + self._storeKey(name, kid, pem, dsname, callback) }) - }) + } }) }) }) @@ -365,76 +361,85 @@ class Keychain { } /** - * Export an existing key as a PEM encrypted PKCS #8 string + * Export an existing key. + * If it's as an RSA key, include a password to export as a PEM encrypted PKCS #8 string * * @param {string} name - The local key name; must already exist. - * @param {string} password - The password + * @param {string} password - The password, for RSA keys (optional) * @param {function(Error, string)} callback * @returns {undefined} */ exportKey (name, password, callback) { + if (typeof password === 'function' && typeof callback === 'undefined') { + callback = password + password = undefined + } if (!validateKeyName(name)) { return _error(callback, `Invalid key name '${name}'`) } - if (!password) { - return _error(callback, 'Password is required') - } const dsname = DsName(name) this.store.get(dsname, (err, res) => { if (err) { return _error(callback, `Key '${name}' does not exist. ${err.message}`) } - const pem = res.toString() - crypto.keys.import(pem, this._(), (err, privateKey) => { - if (err) return _error(callback, err) - privateKey.export(password, callback) - }) + if (password) { + const encKey = res.toString() + crypto.keys.import(encKey, this._(), (err, privateKey) => { + if (err) return _error(callback, err) + privateKey.export(password, callback) + }) + } else { + crypto.keys.unmarshalPrivateKey(res, callback) + } }) } /** - * Import a new key from a PEM encoded PKCS #8 string + * Import a new key + * If it's as an RSA key, include a password to import from a PEM encrypted 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. + * @param {string} encKey - The encoded key. If it's an RSA key, it needs to be a PEM encoded PKCS #8 string + * @param {string} password - The password for RSA keys. (optional) * @param {function(Error, KeyInfo)} callback * @returns {undefined} */ - importKey (name, pem, password, callback) { + importKey (name, encKey, password, callback) { const self = this + if (typeof password === 'function' && typeof callback === 'undefined') { + callback = password + password = undefined + } if (!validateKeyName(name) || name === 'self') { return _error(callback, `Invalid key name '${name}'`) } - if (!pem) { - return _error(callback, 'PEM encoded key is required') + if (!encKey) { + return _error(callback, 'The encoded key is required') } + const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - crypto.keys.import(pem, password, (err, privateKey) => { - if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') - 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) + if (password) { + crypto.keys.import(encKey, password, (err, privateKey) => { + if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') + privateKey.id((err, kid) => { + if (err) return _error(callback, err) + privateKey.export(this._(), (err, pem) => { + if (err) return _error(callback, err) + self._storeKey(name, kid, pem, dsname, callback) }) }) }) - }) + } else { + encKey.id((err, kid) => { + if (err) return _error(callback, err) + self._storeKey(name, kid, encKey.bytes, dsname, callback) + }) + } }) } @@ -457,23 +462,28 @@ class Keychain { 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) - }) + self._storeKey(name, kid, pem, dsname, callback) }) }) }) } + _storeKey (name, kid, encKey, dsname, callback) { + const self = this + const keyInfo = { + name: name, + id: kid + } + const batch = self.store.batch() + batch.put(dsname, encKey) + batch.put(DsInfoName(name), JSON.stringify(keyInfo)) + batch.commit((err) => { + if (err) return _error(callback, err) + + callback(null, keyInfo) + }) + } + /** * Gets the private key as PEM encoded PKCS #8 string. * diff --git a/test/keychain.spec.js b/test/keychain.spec.js index ed6f1a80..9e3c6dc6 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -13,9 +13,11 @@ 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 + const keyName = 'tajné jméno' + const renamedKeyName = 'ชื่อลับ' + let keyInfo + let ecKeyInfo + let secpKeyInfo let emptyKeystore let ks @@ -80,23 +82,43 @@ module.exports = (datastore1, datastore2) => { }) describe('key', () => { - it('can be an RSA key', function (done) { + it('can be an ed25519 key', function (done) { this.timeout(50 * 1000) - ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => { + ks.createKey(keyName + 'ed25519', 'ed25519', 2048, (err, info) => { expect(err).to.not.exist() expect(info).exist() - rsaKeyInfo = info + ecKeyInfo = info + done() + }) + }) + + it('can be an secp256k1 key', function (done) { + this.timeout(50 * 1000) + ks.createKey(keyName + 'secp256k1', 'secp256k1', 2048, (err, info) => { + expect(err).to.not.exist() + expect(info).exist() + secpKeyInfo = info + done() + }) + }) + + it('can be an RSA key', function (done) { + this.timeout(50 * 1000) + ks.createKey(keyName, 'rsa', 2048, (err, info) => { + expect(err).to.not.exist() + expect(info).exist() + keyInfo = info done() }) }) it('has a name and id', () => { - expect(rsaKeyInfo).to.have.property('name', rsaKeyName) - expect(rsaKeyInfo).to.have.property('id') + expect(keyInfo).to.have.property('name', keyName) + expect(keyInfo).to.have.property('id') }) it('is encrypted PEM encoded PKCS #8', (done) => { - ks._getPrivateKey(rsaKeyName, (err, pem) => { + ks._getPrivateKey(keyName, (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') done() @@ -104,7 +126,7 @@ module.exports = (datastore1, datastore2) => { }) it('does not overwrite existing key', (done) => { - ks.createKey(rsaKeyName, 'rsa', 2048, (err) => { + ks.createKey(keyName, 'rsa', 2048, (err) => { expect(err).to.exist() done() }) @@ -157,26 +179,26 @@ module.exports = (datastore1, datastore2) => { ks.listKeys((err, keys) => { expect(err).to.not.exist() expect(keys).to.exist() - const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) + const mykey = keys.find((k) => k.name.normalize() === keyName.normalize()) expect(mykey).to.exist() done() }) }) it('finds a key by name', (done) => { - ks.findKeyByName(rsaKeyName, (err, key) => { + ks.findKeyByName(keyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) + expect(key).to.deep.equal(keyInfo) done() }) }) it('finds a key by id', (done) => { - ks.findKeyById(rsaKeyInfo.id, (err, key) => { + ks.findKeyById(keyInfo.id, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(rsaKeyInfo) + expect(key).to.deep.equal(keyInfo) done() }) }) @@ -211,14 +233,14 @@ module.exports = (datastore1, datastore2) => { }) it('requires plain data as a Buffer', (done) => { - ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => { + ks.cms.encrypt(keyName, 'plain data', (err, msg) => { expect(err).to.exist() done() }) }) it('encrypts', (done) => { - ks.cms.encrypt(rsaKeyName, plainData, (err, msg) => { + ks.cms.encrypt(keyName, plainData, (err, msg) => { expect(err).to.not.exist() expect(msg).to.exist() expect(msg).to.be.instanceOf(Buffer) @@ -245,7 +267,7 @@ module.exports = (datastore1, datastore2) => { 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.missingKeys).to.eql([keyInfo.id]) done() }) }) @@ -262,9 +284,11 @@ module.exports = (datastore1, datastore2) => { describe('exported key', () => { let pemKey + let ed25519Key + let secp256k1Key it('is a PKCS #8 encrypted pem', (done) => { - ks.exportKey(rsaKeyName, 'password', (err, pem) => { + ks.exportKey(keyName, 'password', (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') pemKey = pem @@ -276,13 +300,49 @@ module.exports = (datastore1, datastore2) => { 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) + expect(key.id).to.equal(keyInfo.id) + done() + }) + }) + + it('can export ed25519 key', (done) => { + ks.exportKey(keyName + 'ed25519', (err, key) => { + expect(err).to.not.exist() + ed25519Key = key + expect(key).to.exist() + done() + }) + }) + + it('ed25519 key can be imported', (done) => { + ks.importKey('imported-key-ed25199', ed25519Key, (err, key) => { + expect(err).to.not.exist() + expect(key.name).to.equal('imported-key-ed25199') + expect(key.id).to.equal(ecKeyInfo.id) + done() + }) + }) + + it('can export secp256k1 key', (done) => { + ks.exportKey(keyName + 'secp256k1', (err, key) => { + expect(err).to.not.exist() + secp256k1Key = key + expect(key).to.exist() + done() + }) + }) + + it('secp256k1 key can be imported', (done) => { + ks.importKey('imported-key-secp256k1', secp256k1Key, (err, key) => { + expect(err).to.not.exist() + expect(key.name).to.equal('imported-key-secp256k1') + expect(key.id).to.equal(secpKeyInfo.id) done() }) }) it('cannot be imported as an existing key name', (done) => { - ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => { + ks.importKey(keyName, pemKey, 'password', (err, key) => { expect(err).to.exist() done() }) @@ -342,40 +402,40 @@ module.exports = (datastore1, datastore2) => { describe('rename', () => { it('requires an existing key name', (done) => { - ks.renameKey('not-there', renamedRsaKeyName, (err) => { + ks.renameKey('not-there', renamedKeyName, (err) => { expect(err).to.exist() done() }) }) it('requires a valid new key name', (done) => { - ks.renameKey(rsaKeyName, '..\not-valid', (err) => { + ks.renameKey(keyName, '..\not-valid', (err) => { expect(err).to.exist() done() }) }) it('does not overwrite existing key', (done) => { - ks.renameKey(rsaKeyName, rsaKeyName, (err) => { + ks.renameKey(keyName, keyName, (err) => { expect(err).to.exist() done() }) }) it('cannot create the "self" key', (done) => { - ks.renameKey(rsaKeyName, 'self', (err) => { + ks.renameKey(keyName, 'self', (err) => { expect(err).to.exist() done() }) }) it('removes the existing key name', (done) => { - ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => { + ks.renameKey(keyName, renamedKeyName, (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(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) + ks.findKeyByName(keyName, (err, key) => { expect(err).to.exist() done() }) @@ -383,20 +443,20 @@ module.exports = (datastore1, datastore2) => { }) it('creates the new key name', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { + ks.findKeyByName(renamedKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('name', renamedKeyName) done() }) }) it('does not change the key ID', (done) => { - ks.findKeyByName(renamedRsaKeyName, (err, key) => { + ks.findKeyByName(renamedKeyName, (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) + expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) done() }) }) @@ -418,11 +478,11 @@ module.exports = (datastore1, datastore2) => { }) it('can remove a known key', (done) => { - ks.removeKey(renamedRsaKeyName, (err, key) => { + ks.removeKey(renamedKeyName, (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) + expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('id', keyInfo.id) done() }) }) From 217cfd3de897afa375dcc5bd83db135a3c4c8bf9 Mon Sep 17 00:00:00 2001 From: Alberto Elias Date: Tue, 26 Feb 2019 12:22:59 +0100 Subject: [PATCH 59/87] chore: update libp2p-crypto (#33) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9055b7cc..11333071 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "async": "^2.6.1", "interface-datastore": "~0.6.0", - "libp2p-crypto": "~0.16.0", + "libp2p-crypto": "~0.16.1", "merge-options": "^1.0.1", "node-forge": "~0.7.6", "pull-stream": "^3.6.8", From 267002f646f41d7a9d801ffb771e3d9963baf56d Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 26 Feb 2019 11:39:49 +0000 Subject: [PATCH 60/87] chore: update contributors --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 11333071..21b2a6fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.3.6", + "version": "0.4.0", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -65,13 +65,14 @@ }, "contributors": [ "Alan Shaw ", + "Alberto Elias ", "David Dias ", "Hugo Dias ", "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", - "Vasco Santos ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From e30330e1a0e179d70eb2c082c0a955bf1b4847cf Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 26 Feb 2019 11:39:50 +0000 Subject: [PATCH 61/87] chore: release version v0.4.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f9af609..7ad54ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +# [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26) + + +### Features + +* adds support for ed25199 and secp256k1 ([#31](https://github.com/libp2p/js-libp2p-keychain/issues/31)) ([9eb11f4](https://github.com/libp2p/js-libp2p-keychain/commit/9eb11f4)) + + + ## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10) From f71a6bbb0a044f6325bae3d1ea10b6b1ceda8c9f Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 14 Mar 2019 22:26:07 +0000 Subject: [PATCH 62/87] Revert "feat: adds support for ed25199 and secp256k1 (#31)" This reverts commit 9eb11f42452b4266db0c19de3310e809a0087669. --- README.md | 6 +- src/keychain.js | 128 +++++++++++++++++++--------------------- test/keychain.spec.js | 132 ++++++++++++------------------------------ 3 files changed, 98 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 655ff03d..24bcf0d6 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Managing a key - `createKey (name, type, size, callback)` - `renameKey (oldName, newName, callback)` - `removeKey (name, callback)` -- `exportKey (name, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys -- `importKey (name, encKey, password, callback)` // Omit _password_ for `ed25199` or `secp256k1` keys +- `exportKey (name, password, callback)` +- `importKey (name, pem, password, callback)` - `importPeer (name, peer, callback)` A naming service for a key @@ -67,7 +67,7 @@ A naming service for a key - `findKeyById (id, callback)` - `findKeyByName (name, callback)` -Cryptographically protected messages (Only supported with RSA keys) +Cryptographically protected messages - `cms.encrypt (name, plain, callback)` - `cms.decrypt (cmsData, callback)` diff --git a/src/keychain.js b/src/keychain.js index e236d8a8..cecc3207 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -20,7 +20,7 @@ const NIST = { } const defaultOptions = { - // See https://cryptosense.com/blog/parameter-choice-for-pbkdf2/ + // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ dek: { keyLength: 512 / 8, iterationCount: 10000, @@ -197,8 +197,7 @@ class Keychain { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - type = type.toLowerCase() - switch (type) { + switch (type.toLowerCase()) { case 'rsa': if (size < 2048) { return _error(callback, `Invalid RSA key size ${size}`) @@ -212,16 +211,21 @@ class Keychain { if (err) return _error(callback, err) keypair.id((err, kid) => { if (err) return _error(callback, err) - - if (type === 'ed25519' || type === 'secp256k1') { - const keypairMarshal = keypair.bytes - self._storeKey(name, kid, keypairMarshal, dsname, callback) - } else { - keypair.export(this._(), (err, pem) => { + 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) - self._storeKey(name, kid, pem, dsname, callback) + + callback(null, keyInfo) }) - } + }) }) }) }) @@ -361,85 +365,76 @@ class Keychain { } /** - * Export an existing key. - * If it's as an RSA key, include a password to export as a PEM encrypted PKCS #8 string + * 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, for RSA keys (optional) + * @param {string} password - The password * @param {function(Error, string)} callback * @returns {undefined} */ exportKey (name, password, callback) { - if (typeof password === 'function' && typeof callback === 'undefined') { - callback = password - password = undefined - } if (!validateKeyName(name)) { return _error(callback, `Invalid key name '${name}'`) } + if (!password) { + return _error(callback, 'Password is required') + } const dsname = DsName(name) this.store.get(dsname, (err, res) => { if (err) { return _error(callback, `Key '${name}' does not exist. ${err.message}`) } - if (password) { - const encKey = res.toString() - crypto.keys.import(encKey, this._(), (err, privateKey) => { - if (err) return _error(callback, err) - privateKey.export(password, callback) - }) - } else { - crypto.keys.unmarshalPrivateKey(res, callback) - } + const pem = res.toString() + crypto.keys.import(pem, this._(), (err, privateKey) => { + if (err) return _error(callback, err) + privateKey.export(password, callback) + }) }) } /** - * Import a new key - * If it's as an RSA key, include a password to import from a PEM encrypted PKCS #8 string + * Import a new key from a PEM encoded PKCS #8 string * * @param {string} name - The local key name; must not already exist. - * @param {string} encKey - The encoded key. If it's an RSA key, it needs to be a PEM encoded PKCS #8 string - * @param {string} password - The password for RSA keys. (optional) + * @param {string} pem - The PEM encoded PKCS #8 string + * @param {string} password - The password. * @param {function(Error, KeyInfo)} callback * @returns {undefined} */ - importKey (name, encKey, password, callback) { + importKey (name, pem, password, callback) { const self = this - if (typeof password === 'function' && typeof callback === 'undefined') { - callback = password - password = undefined - } if (!validateKeyName(name) || name === 'self') { return _error(callback, `Invalid key name '${name}'`) } - if (!encKey) { - return _error(callback, 'The encoded key is required') + if (!pem) { + return _error(callback, 'PEM encoded key is required') } - const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) if (exists) return _error(callback, `Key '${name}' already exists`) - - if (password) { - crypto.keys.import(encKey, password, (err, privateKey) => { - if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') - privateKey.id((err, kid) => { + crypto.keys.import(pem, password, (err, privateKey) => { + if (err) return _error(callback, 'Cannot read the key, most likely the password is wrong') + privateKey.id((err, kid) => { + if (err) return _error(callback, err) + privateKey.export(this._(), (err, pem) => { if (err) return _error(callback, err) - privateKey.export(this._(), (err, pem) => { + 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) - self._storeKey(name, kid, pem, dsname, callback) + + callback(null, keyInfo) }) }) }) - } else { - encKey.id((err, kid) => { - if (err) return _error(callback, err) - self._storeKey(name, kid, encKey.bytes, dsname, callback) - }) - } + }) }) } @@ -462,28 +457,23 @@ class Keychain { if (err) return _error(callback, err) privateKey.export(this._(), (err, pem) => { if (err) return _error(callback, err) - self._storeKey(name, kid, pem, dsname, callback) + 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) + }) }) }) }) } - _storeKey (name, kid, encKey, dsname, callback) { - const self = this - const keyInfo = { - name: name, - id: kid - } - const batch = self.store.batch() - batch.put(dsname, encKey) - batch.put(DsInfoName(name), JSON.stringify(keyInfo)) - batch.commit((err) => { - if (err) return _error(callback, err) - - callback(null, keyInfo) - }) - } - /** * Gets the private key as PEM encoded PKCS #8 string. * diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 9e3c6dc6..ed6f1a80 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -13,11 +13,9 @@ const PeerId = require('peer-id') module.exports = (datastore1, datastore2) => { describe('keychain', () => { const passPhrase = 'this is not a secure phrase' - const keyName = 'tajné jméno' - const renamedKeyName = 'ชื่อลับ' - let keyInfo - let ecKeyInfo - let secpKeyInfo + const rsaKeyName = 'tajné jméno' + const renamedRsaKeyName = 'ชื่อลับ' + let rsaKeyInfo let emptyKeystore let ks @@ -82,43 +80,23 @@ module.exports = (datastore1, datastore2) => { }) describe('key', () => { - it('can be an ed25519 key', function (done) { - this.timeout(50 * 1000) - ks.createKey(keyName + 'ed25519', 'ed25519', 2048, (err, info) => { - expect(err).to.not.exist() - expect(info).exist() - ecKeyInfo = info - done() - }) - }) - - it('can be an secp256k1 key', function (done) { - this.timeout(50 * 1000) - ks.createKey(keyName + 'secp256k1', 'secp256k1', 2048, (err, info) => { - expect(err).to.not.exist() - expect(info).exist() - secpKeyInfo = info - done() - }) - }) - it('can be an RSA key', function (done) { this.timeout(50 * 1000) - ks.createKey(keyName, 'rsa', 2048, (err, info) => { + ks.createKey(rsaKeyName, 'rsa', 2048, (err, info) => { expect(err).to.not.exist() expect(info).exist() - keyInfo = info + rsaKeyInfo = info done() }) }) it('has a name and id', () => { - expect(keyInfo).to.have.property('name', keyName) - expect(keyInfo).to.have.property('id') + expect(rsaKeyInfo).to.have.property('name', rsaKeyName) + expect(rsaKeyInfo).to.have.property('id') }) it('is encrypted PEM encoded PKCS #8', (done) => { - ks._getPrivateKey(keyName, (err, pem) => { + ks._getPrivateKey(rsaKeyName, (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') done() @@ -126,7 +104,7 @@ module.exports = (datastore1, datastore2) => { }) it('does not overwrite existing key', (done) => { - ks.createKey(keyName, 'rsa', 2048, (err) => { + ks.createKey(rsaKeyName, 'rsa', 2048, (err) => { expect(err).to.exist() done() }) @@ -179,26 +157,26 @@ module.exports = (datastore1, datastore2) => { ks.listKeys((err, keys) => { expect(err).to.not.exist() expect(keys).to.exist() - const mykey = keys.find((k) => k.name.normalize() === keyName.normalize()) + const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) expect(mykey).to.exist() done() }) }) it('finds a key by name', (done) => { - ks.findKeyByName(keyName, (err, key) => { + ks.findKeyByName(rsaKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(keyInfo) + expect(key).to.deep.equal(rsaKeyInfo) done() }) }) it('finds a key by id', (done) => { - ks.findKeyById(keyInfo.id, (err, key) => { + ks.findKeyById(rsaKeyInfo.id, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.deep.equal(keyInfo) + expect(key).to.deep.equal(rsaKeyInfo) done() }) }) @@ -233,14 +211,14 @@ module.exports = (datastore1, datastore2) => { }) it('requires plain data as a Buffer', (done) => { - ks.cms.encrypt(keyName, 'plain data', (err, msg) => { + ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => { expect(err).to.exist() done() }) }) it('encrypts', (done) => { - ks.cms.encrypt(keyName, plainData, (err, msg) => { + ks.cms.encrypt(rsaKeyName, plainData, (err, msg) => { expect(err).to.not.exist() expect(msg).to.exist() expect(msg).to.be.instanceOf(Buffer) @@ -267,7 +245,7 @@ module.exports = (datastore1, datastore2) => { emptyKeystore.cms.decrypt(cms, (err, plain) => { expect(err).to.exist() expect(err).to.have.property('missingKeys') - expect(err.missingKeys).to.eql([keyInfo.id]) + expect(err.missingKeys).to.eql([rsaKeyInfo.id]) done() }) }) @@ -284,11 +262,9 @@ module.exports = (datastore1, datastore2) => { describe('exported key', () => { let pemKey - let ed25519Key - let secp256k1Key it('is a PKCS #8 encrypted pem', (done) => { - ks.exportKey(keyName, 'password', (err, pem) => { + ks.exportKey(rsaKeyName, 'password', (err, pem) => { expect(err).to.not.exist() expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') pemKey = pem @@ -300,49 +276,13 @@ module.exports = (datastore1, datastore2) => { 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(keyInfo.id) - done() - }) - }) - - it('can export ed25519 key', (done) => { - ks.exportKey(keyName + 'ed25519', (err, key) => { - expect(err).to.not.exist() - ed25519Key = key - expect(key).to.exist() - done() - }) - }) - - it('ed25519 key can be imported', (done) => { - ks.importKey('imported-key-ed25199', ed25519Key, (err, key) => { - expect(err).to.not.exist() - expect(key.name).to.equal('imported-key-ed25199') - expect(key.id).to.equal(ecKeyInfo.id) - done() - }) - }) - - it('can export secp256k1 key', (done) => { - ks.exportKey(keyName + 'secp256k1', (err, key) => { - expect(err).to.not.exist() - secp256k1Key = key - expect(key).to.exist() - done() - }) - }) - - it('secp256k1 key can be imported', (done) => { - ks.importKey('imported-key-secp256k1', secp256k1Key, (err, key) => { - expect(err).to.not.exist() - expect(key.name).to.equal('imported-key-secp256k1') - expect(key.id).to.equal(secpKeyInfo.id) + expect(key.id).to.equal(rsaKeyInfo.id) done() }) }) it('cannot be imported as an existing key name', (done) => { - ks.importKey(keyName, pemKey, 'password', (err, key) => { + ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => { expect(err).to.exist() done() }) @@ -402,40 +342,40 @@ module.exports = (datastore1, datastore2) => { describe('rename', () => { it('requires an existing key name', (done) => { - ks.renameKey('not-there', renamedKeyName, (err) => { + ks.renameKey('not-there', renamedRsaKeyName, (err) => { expect(err).to.exist() done() }) }) it('requires a valid new key name', (done) => { - ks.renameKey(keyName, '..\not-valid', (err) => { + ks.renameKey(rsaKeyName, '..\not-valid', (err) => { expect(err).to.exist() done() }) }) it('does not overwrite existing key', (done) => { - ks.renameKey(keyName, keyName, (err) => { + ks.renameKey(rsaKeyName, rsaKeyName, (err) => { expect(err).to.exist() done() }) }) it('cannot create the "self" key', (done) => { - ks.renameKey(keyName, 'self', (err) => { + ks.renameKey(rsaKeyName, 'self', (err) => { expect(err).to.exist() done() }) }) it('removes the existing key name', (done) => { - ks.renameKey(keyName, renamedKeyName, (err, key) => { + ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedKeyName) - expect(key).to.have.property('id', keyInfo.id) - ks.findKeyByName(keyName, (err, key) => { + 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() }) @@ -443,20 +383,20 @@ module.exports = (datastore1, datastore2) => { }) it('creates the new key name', (done) => { - ks.findKeyByName(renamedKeyName, (err, key) => { + ks.findKeyByName(renamedRsaKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedKeyName) + expect(key).to.have.property('name', renamedRsaKeyName) done() }) }) it('does not change the key ID', (done) => { - ks.findKeyByName(renamedKeyName, (err, key) => { + ks.findKeyByName(renamedRsaKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedKeyName) - expect(key).to.have.property('id', keyInfo.id) + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) done() }) }) @@ -478,11 +418,11 @@ module.exports = (datastore1, datastore2) => { }) it('can remove a known key', (done) => { - ks.removeKey(renamedKeyName, (err, key) => { + ks.removeKey(renamedRsaKeyName, (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - expect(key).to.have.property('name', renamedKeyName) - expect(key).to.have.property('id', keyInfo.id) + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) done() }) }) From 4e4d3d4b6f25e28a7bb92f9d777d8bdde6f8e1d9 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 14 Mar 2019 22:38:45 +0000 Subject: [PATCH 63/87] chore: update contributors --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 21b2a6fd..97176ef9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.4.0", + "version": "0.4.1", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -71,8 +71,8 @@ "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", - "Vasco Santos ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From a5fd967c028efc54effa92f597918fc98c462320 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 14 Mar 2019 22:38:45 +0000 Subject: [PATCH 64/87] chore: release version v0.4.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad54ab2..7df7adbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14) + + + # [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26) From ef4737494115d6bb4f75eada0df4eac449f4daeb Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 11 Apr 2019 11:20:18 +0100 Subject: [PATCH 65/87] chore: add discourse badge (#34) --- README.md | 1 + package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 24bcf0d6..4ff4abe6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![](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) +[![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) diff --git a/package.json b/package.json index 97176ef9..e048791a 100644 --- a/package.json +++ b/package.json @@ -42,22 +42,22 @@ }, "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { - "async": "^2.6.1", + "async": "^2.6.2", "interface-datastore": "~0.6.0", "libp2p-crypto": "~0.16.1", "merge-options": "^1.0.1", "node-forge": "~0.7.6", - "pull-stream": "^3.6.8", + "pull-stream": "^3.6.9", "sanitize-filename": "^1.6.1" }, "devDependencies": { - "aegir": "^18.0.3", + "aegir": "^18.2.1", "chai": "^4.2.0", - "chai-string": "^1.4.0", - "datastore-fs": "~0.7.0", + "chai-string": "^1.5.0", + "datastore-fs": "~0.8.0", "datastore-level": "~0.10.0", "dirty-chai": "^2.0.1", - "level-js": "^4.0.0", + "level-js": "^4.0.1", "mocha": "^5.2.0", "multihashes": "~0.4.14", "peer-id": "~0.12.2", From 7051b9c530821e1fd21e28fffee5a17f1e2d46e9 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 13 Jun 2019 14:35:12 +0100 Subject: [PATCH 66/87] fix: throw errors with correct stack trace (#35) The stack trace of thrown error objects is created when the object is instantiated - if we defer to a function to create the error we end up with misleading stack traces. This PR instantiates errors where errors occur and also uses the `err-code` module to add a `.code` property so we don't have to depend on string error messages for the type of error that was thrown. --- package.json | 1 + src/cms.js | 14 +++++++------ src/keychain.js | 49 ++++++++++++++++++++++--------------------- test/keychain.spec.js | 14 +++++++++++++ 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e048791a..7521e6d0 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "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", "merge-options": "^1.0.1", diff --git a/src/cms.js b/src/cms.js index 90d7d85f..d086407e 100644 --- a/src/cms.js +++ b/src/cms.js @@ -8,6 +8,7 @@ require('node-forge/lib/pkcs7') require('node-forge/lib/pbe') const forge = require('node-forge/lib/forge') const util = require('./util') +const errcode = require('err-code') /** * Cryptographic Message Syntax (aka PKCS #7) @@ -26,7 +27,7 @@ class CMS { */ constructor (keychain) { if (!keychain) { - throw new Error('keychain is required') + throw errcode(new Error('keychain is required'), 'ERR_KEYCHAIN_REQUIRED') } this.keychain = keychain @@ -47,7 +48,7 @@ class CMS { const done = (err, result) => setImmediate(() => callback(err, result)) if (!Buffer.isBuffer(plain)) { - return done(new Error('Plain data must be a Buffer')) + return done(errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')) } series([ @@ -93,7 +94,7 @@ class CMS { const done = (err, result) => setImmediate(() => callback(err, result)) if (!Buffer.isBuffer(cmsData)) { - return done(new Error('CMS data is required')) + return done(errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS')) } const self = this @@ -103,7 +104,7 @@ class CMS { const obj = forge.asn1.fromDer(buf) cms = forge.pkcs7.messageFromAsn1(obj) } catch (err) { - return done(new Error('Invalid CMS: ' + err.message)) + return done(errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS')) } // Find a recipient whose key we hold. We only deal with recipient certs @@ -124,8 +125,9 @@ class CMS { if (err) return done(err) if (!r) { const missingKeys = recipients.map(r => r.keyId) - err = new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')) - err.missingKeys = missingKeys + err = errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', { + missingKeys + }) return done(err) } diff --git a/src/keychain.js b/src/keychain.js index cecc3207..f8f88895 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -8,6 +8,7 @@ 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') const keyPrefix = '/pkcs8/' const infoPrefix = '/info/' @@ -50,7 +51,7 @@ function _error (callback, err) { const min = 200 const max = 1000 const delay = Math.random() * (max - min) + min - if (typeof err === 'string') err = new Error(err) + setTimeout(callback, delay, err, null) } @@ -181,26 +182,26 @@ class Keychain { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (typeof type !== 'string') { - return _error(callback, `Invalid key type '${type}'`) + return _error(callback, errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE')) } if (!Number.isSafeInteger(size)) { - return _error(callback, `Invalid key size '${size}'`) + return _error(callback, 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, `Key '${name}' already exists`) + 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, `Invalid RSA key size ${size}`) + return _error(callback, errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE')) } break default: @@ -278,13 +279,13 @@ class Keychain { */ findKeyByName (name, callback) { if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, 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, `Key '${name}' does not exist. ${err.message}`) + return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) } callback(null, JSON.parse(res.toString())) @@ -301,7 +302,7 @@ class Keychain { removeKey (name, callback) { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } const dsname = DsName(name) self.findKeyByName(name, (err, keyinfo) => { @@ -327,10 +328,10 @@ class Keychain { renameKey (oldName, newName, callback) { const self = this if (!validateKeyName(oldName) || oldName === 'self') { - return _error(callback, `Invalid old key name '${oldName}'`) + return _error(callback, errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID')) } if (!validateKeyName(newName) || newName === 'self') { - return _error(callback, `Invalid new key name '${newName}'`) + return _error(callback, errcode(new Error(`Invalid new key name '${newName}'`), 'ERR_NEW_KEY_NAME_INVALID')) } const oldDsname = DsName(oldName) const newDsname = DsName(newName) @@ -338,12 +339,12 @@ class Keychain { const newInfoName = DsInfoName(newName) this.store.get(oldDsname, (err, res) => { if (err) { - return _error(callback, `Key '${oldName}' does not exist. ${err.message}`) + return _error(callback, errcode(new Error(`Key '${oldName}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) } const pem = res.toString() self.store.has(newDsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${newName}' already exists`) + 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) @@ -374,16 +375,16 @@ class Keychain { */ exportKey (name, password, callback) { if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!password) { - return _error(callback, 'Password is required') + return _error(callback, errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED')) } const dsname = DsName(name) this.store.get(dsname, (err, res) => { if (err) { - return _error(callback, `Key '${name}' does not exist. ${err.message}`) + return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) } const pem = res.toString() crypto.keys.import(pem, this._(), (err, privateKey) => { @@ -405,7 +406,7 @@ class Keychain { importKey (name, pem, password, callback) { const self = this if (!validateKeyName(name) || name === 'self') { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!pem) { return _error(callback, 'PEM encoded key is required') @@ -413,9 +414,9 @@ class Keychain { const dsname = DsName(name) self.store.has(dsname, (err, exists) => { if (err) return _error(callback, err) - if (exists) return _error(callback, `Key '${name}' already exists`) + 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, 'Cannot read the key, most likely the password is wrong') + 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) => { @@ -441,17 +442,17 @@ class Keychain { importPeer (name, peer, callback) { const self = this if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } if (!peer || !peer.privKey) { - return _error(callback, 'Peer.privKey is required') + return _error(callback, 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, `Key '${name}' already exists`) + 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) @@ -484,11 +485,11 @@ class Keychain { */ _getPrivateKey (name, callback) { if (!validateKeyName(name)) { - return _error(callback, `Invalid key name '${name}'`) + return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME')) } this.store.get(DsName(name), (err, res) => { if (err) { - return _error(callback, `Key '${name}' does not exist. ${err.message}`) + return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND')) } callback(null, res.toString()) }) diff --git a/test/keychain.spec.js b/test/keychain.spec.js index ed6f1a80..bcaa6671 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -59,22 +59,27 @@ module.exports = (datastore1, datastore2) => { 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') }) }) }) @@ -106,6 +111,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -146,6 +152,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -246,6 +253,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -344,6 +352,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -351,6 +360,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -358,6 +368,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -365,6 +376,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -406,6 +418,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) @@ -413,6 +426,7 @@ module.exports = (datastore1, datastore2) => { 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() }) }) From 74cb4d47754d0d96c1929c125eaaa78a1f047930 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 13 Jun 2019 14:50:37 +0100 Subject: [PATCH 67/87] chore: update contributors --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7521e6d0..b6526b35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.4.1", + "version": "0.4.2", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -67,13 +67,14 @@ "contributors": [ "Alan Shaw ", "Alberto Elias ", + "Alex Potsides ", "David Dias ", "Hugo Dias ", "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", - "Vasco Santos ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From 717112bdf84db2df647be7993e3d6cdea3428d66 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 13 Jun 2019 14:50:37 +0100 Subject: [PATCH 68/87] chore: release version v0.4.2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df7adbd..4b40f5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13) + + +### Bug Fixes + +* throw errors with correct stack trace ([#35](https://github.com/libp2p/js-libp2p-keychain/issues/35)) ([7051b9c](https://github.com/libp2p/js-libp2p-keychain/commit/7051b9c)) + + + ## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14) From dda315a9c8972a42b5bd7ef72afbb1a8f8f44f10 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 16 Aug 2019 13:12:47 +0200 Subject: [PATCH 69/87] 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 | 365 ++++++++++++++---------------- 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, 532 insertions(+), 691 deletions(-) diff --git a/.gitignore b/.gitignore index b64f0852..3da57e81 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 37005a25..2061bd32 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 4ff4abe6..37829b48 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 b6526b35..eb3d3085 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 d086407e..9bec4b94 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) + 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) - 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) + // 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 f8f88895..2f67345c 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')) + 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 _error(callback, errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE')) - } - break - default: - break + 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) + await batch.commit() + } catch (err) { + return throwDelayed(err) + } - callback(null, keyInfo) - }) - }) - }) - }) - }) + 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')) + res = await self.store.get(oldInfoName) - 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) - }) - }) - }) - }) + 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) + const exists = await self.store.has(dsname) + if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) - callback(null, keyInfo) - }) - }) - }) - }) - }) + 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')) + const exists = await self.store.has(dsname) + if (exists) return throwDelayed(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) - }) - }) - }) - }) + 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 bc61c5b7..50ce4174 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 0a37bedd..02222fb3 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 a7449984..06eb6312 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 bcaa6671..c455f2d7 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 e11d0744..f67b6b64 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 7d6588cb..74ba9bf6 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() }) }) From e375c2f1e8b5f2972c5bc0156c209b9d7cecd606 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 16 Aug 2019 14:25:02 +0100 Subject: [PATCH 70/87] chore: update contributors --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index eb3d3085..450281fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.4.2", + "version": "0.5.0", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", @@ -66,11 +66,12 @@ "Alex Potsides ", "David Dias ", "Hugo Dias ", + "Jacob Heun ", "Maciej Krüger ", "Masahiro Saito ", "Richard Schneider ", - "Vasco Santos ", "Vasco Santos ", + "Vasco Santos ", "Victor Bjelkholm " ] } From ad378174f79fb0bfbda75c4b12eb817d2960d27c Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 16 Aug 2019 14:25:02 +0100 Subject: [PATCH 71/87] chore: release version v0.5.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b40f5b1..c2bd4f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + +# [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16) + + +* refactor: use async/await instead of callbacks (#37) ([dda315a](https://github.com/libp2p/js-libp2p-keychain/commit/dda315a)), closes [#37](https://github.com/libp2p/js-libp2p-keychain/issues/37) + + +### BREAKING CHANGES + +* The api now uses async/await instead of callbacks. + +Co-Authored-By: Vasco Santos + + + ## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13) From 893a2c975c098675f3841a13f2a6a3aa7e95ddf9 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Wed, 25 Sep 2019 11:19:38 +0100 Subject: [PATCH 72/87] chore: downgrade peer-id to same version used by libp2p (#38) --- package.json | 4 ++-- src/keychain.js | 41 +++++++++++++++++++++++++++++++---------- test/keychain.spec.js | 3 ++- test/node.js | 4 ++-- test/peerid.js | 21 +++++++++++++++------ 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 450281fd..8a86f819 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "err-code": "^2.0.0", "interface-datastore": "^0.7.0", - "libp2p-crypto": "^0.17.0", + "libp2p-crypto": "^0.16.2", "merge-options": "^1.0.1", "node-forge": "^0.8.5", "sanitize-filename": "^1.6.1" @@ -56,7 +56,7 @@ "dirty-chai": "^2.0.1", "level": "^5.0.1", "multihashes": "^0.4.15", - "peer-id": "^0.13.2", + "peer-id": "^0.12.2", "promisify-es6": "^1.0.3", "rimraf": "^2.6.3" }, diff --git a/src/keychain.js b/src/keychain.js index 2f67345c..7bd8ba34 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,6 +5,7 @@ const sanitize = require('sanitize-filename') const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') +const promisify = require('promisify-es6') const CMS = require('./cms') const errcode = require('err-code') @@ -205,10 +206,16 @@ class Keychain { let keyInfo try { - const keypair = await crypto.keys.generateKeyPair(type, size) + const keypair = await promisify(crypto.keys.generateKeyPair, { + context: crypto.keys + })(type, size) - const kid = await keypair.id() - const pem = await keypair.export(this._()) + const kid = await promisify(keypair.id, { + context: keypair + })() + const pem = await promisify(keypair.export, { + context: keypair + })(this._()) keyInfo = { name: name, id: kid @@ -360,8 +367,12 @@ class Keychain { try { const res = await this.store.get(dsname) const pem = res.toString() - const privateKey = await crypto.keys.import(pem, this._()) - return privateKey.export(password) + const privateKey = await promisify(crypto.keys.import, { + context: crypto.keys + })(pem, this._()) + return promisify(privateKey.export, { + context: privateKey + })(password) } catch (err) { return throwDelayed(err) } @@ -389,15 +400,21 @@ class Keychain { let privateKey try { - privateKey = await crypto.keys.import(pem, password) + privateKey = await promisify(crypto.keys.import, { + context: crypto.keys + })(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._()) + kid = await promisify(privateKey.id, { + context: privateKey + })() + pem = await promisify(privateKey.export, { + context: privateKey + })(this._()) } catch (err) { return throwDelayed(err) } @@ -429,8 +446,12 @@ class Keychain { 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 kid = await promisify(privateKey.id, { + context: privateKey + })() + const pem = await promisify(privateKey.export, { + context: privateKey + })(this._()) const keyInfo = { name: name, id: kid diff --git a/test/keychain.spec.js b/test/keychain.spec.js index c455f2d7..0756f5f2 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -9,6 +9,7 @@ chai.use(require('dirty-chai')) chai.use(require('chai-string')) const Keychain = require('../') const PeerId = require('peer-id') +const promisify = require('promisify-es6') module.exports = (datastore1, datastore2) => { describe('keychain', () => { @@ -268,7 +269,7 @@ module.exports = (datastore1, datastore2) => { before(async function () { const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await PeerId.createFromPrivKey(encoded) + alice = await promisify(PeerId.createFromPrivKey)(encoded) }) it('private key can be imported', async () => { diff --git a/test/node.js b/test/node.js index f67b6b64..bbb25089 100644 --- a/test/node.js +++ b/test/node.js @@ -8,8 +8,8 @@ const rimraf = promisify(require('rimraf')) const FsStore = require('datastore-fs') describe('node', () => { - const store1 = path.join(os.tmpdir(), 'test-keystore-1') - const store2 = path.join(os.tmpdir(), 'test-keystore-2') + 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) diff --git a/test/peerid.js b/test/peerid.js index 74ba9bf6..42274db0 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -10,6 +10,7 @@ const multihash = require('multihashes') const crypto = require('libp2p-crypto') const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') const rsaClass = require('libp2p-crypto/src/keys/rsa-class') +const promisify = require('promisify-es6') const sample = { id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', @@ -23,10 +24,10 @@ describe('peer ID', () => { before(async () => { const encoded = Buffer.from(sample.privKey, 'base64') - peer = await PeerId.createFromPrivKey(encoded) + peer = await promisify(PeerId.createFromPrivKey)(encoded) }) - it('decoded public key', () => { + it('decoded public key', async () => { // get protobuf version of the public key const publicKeyProtobuf = peer.marshalPubKey() const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf) @@ -34,14 +35,18 @@ describe('peer ID', () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + const key = await promisify(crypto.keys.unmarshalPrivateKey, { + context: crypto.keys + })(privateKeyProtobuf) expect(key).to.exist() }) it('encoded public key with DER', async () => { const jwk = rsaUtils.pkixToJwk(publicKeyDer) const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await rsa.hash() + const keyId = await promisify(rsa.hash, { + context: rsa + })() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -55,7 +60,9 @@ describe('peer ID', () => { kid: '2011-04-29' } const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await rsa.hash() + const keyId = await promisify(rsa.hash, { + context: rsa + })() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -63,7 +70,9 @@ describe('peer ID', () => { it('decoded private key', async () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + const key = await promisify(crypto.keys.unmarshalPrivateKey, { + context: crypto.keys + })(privateKeyProtobuf) expect(key).to.exist() }) }) From b9eb9d7b4ae36dc1ad8657be3464a7bfe85008de Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 25 Sep 2019 12:33:28 +0200 Subject: [PATCH 73/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a86f819..df74c12c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.5.0", + "version": "0.5.1", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From ce8c412fb6c1ecc087ad332122826d2bd43d7c19 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 25 Sep 2019 12:33:28 +0200 Subject: [PATCH 74/87] chore: release version v0.5.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bd4f3e..12598fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25) + + + # [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16) From 8de96817edada969ce96741a8adff63042e6a48e Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 2 Dec 2019 16:08:08 +0000 Subject: [PATCH 75/87] chore: update node-forge dependency (#39) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df74c12c..cb4a9065 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "interface-datastore": "^0.7.0", "libp2p-crypto": "^0.16.2", "merge-options": "^1.0.1", - "node-forge": "^0.8.5", + "node-forge": "^0.9.1", "sanitize-filename": "^1.6.1" }, "devDependencies": { From ff6bd50350b84fbf60592489e6f6c35e2400a748 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 2 Dec 2019 17:17:38 +0100 Subject: [PATCH 76/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb4a9065..ffed2778 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.5.1", + "version": "0.5.2", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From 163edbbe88d0c6610ce365130ad55203051332f9 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 2 Dec 2019 17:17:38 +0100 Subject: [PATCH 77/87] chore: release version v0.5.2 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12598fe3..dd36aa07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02) + + + ## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25) From b6d5313a550555b435bd301d811af75397dc91e4 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 18 Dec 2019 10:04:20 +0000 Subject: [PATCH 78/87] chore: update deps (#40) --- package.json | 14 +++++++------- src/keychain.js | 42 ++++++++++-------------------------------- test/keychain.spec.js | 3 +-- test/peerid.js | 19 +++++-------------- 4 files changed, 23 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index ffed2778..ff6dbc6d 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "err-code": "^2.0.0", - "interface-datastore": "^0.7.0", - "libp2p-crypto": "^0.16.2", - "merge-options": "^1.0.1", + "interface-datastore": "^0.8.0", + "libp2p-crypto": "^0.17.1", + "merge-options": "^2.0.0", "node-forge": "^0.9.1", "sanitize-filename": "^1.6.1" }, @@ -52,13 +52,13 @@ "chai": "^4.2.0", "chai-string": "^1.5.0", "datastore-fs": "^0.9.0", - "datastore-level": "^0.12.1", + "datastore-level": "^0.14.0", "dirty-chai": "^2.0.1", - "level": "^5.0.1", + "level": "^6.0.0", "multihashes": "^0.4.15", - "peer-id": "^0.12.2", + "peer-id": "^0.13.5", "promisify-es6": "^1.0.3", - "rimraf": "^2.6.3" + "rimraf": "^3.0.0" }, "contributors": [ "Alan Shaw ", diff --git a/src/keychain.js b/src/keychain.js index 7bd8ba34..aae78972 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,7 +5,6 @@ const sanitize = require('sanitize-filename') const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') -const promisify = require('promisify-es6') const CMS = require('./cms') const errcode = require('err-code') @@ -206,16 +205,9 @@ class Keychain { let keyInfo try { - const keypair = await promisify(crypto.keys.generateKeyPair, { - context: crypto.keys - })(type, size) - - const kid = await promisify(keypair.id, { - context: keypair - })() - const pem = await promisify(keypair.export, { - context: keypair - })(this._()) + const keypair = await crypto.keys.generateKeyPair(type, size) + const kid = await keypair.id() + const pem = await keypair.export(this._()) keyInfo = { name: name, id: kid @@ -367,12 +359,8 @@ class Keychain { try { const res = await this.store.get(dsname) const pem = res.toString() - const privateKey = await promisify(crypto.keys.import, { - context: crypto.keys - })(pem, this._()) - return promisify(privateKey.export, { - context: privateKey - })(password) + const privateKey = await crypto.keys.import(pem, this._()) + return privateKey.export(password) } catch (err) { return throwDelayed(err) } @@ -400,21 +388,15 @@ class Keychain { let privateKey try { - privateKey = await promisify(crypto.keys.import, { - context: crypto.keys - })(pem, password) + 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 promisify(privateKey.id, { - context: privateKey - })() - pem = await promisify(privateKey.export, { - context: privateKey - })(this._()) + kid = await privateKey.id() + pem = await privateKey.export(this._()) } catch (err) { return throwDelayed(err) } @@ -446,12 +428,8 @@ class Keychain { if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) try { - const kid = await promisify(privateKey.id, { - context: privateKey - })() - const pem = await promisify(privateKey.export, { - context: privateKey - })(this._()) + const kid = await privateKey.id() + const pem = await privateKey.export(this._()) const keyInfo = { name: name, id: kid diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 0756f5f2..c455f2d7 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -9,7 +9,6 @@ chai.use(require('dirty-chai')) chai.use(require('chai-string')) const Keychain = require('../') const PeerId = require('peer-id') -const promisify = require('promisify-es6') module.exports = (datastore1, datastore2) => { describe('keychain', () => { @@ -269,7 +268,7 @@ module.exports = (datastore1, datastore2) => { before(async function () { const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await promisify(PeerId.createFromPrivKey)(encoded) + alice = await PeerId.createFromPrivKey(encoded) }) it('private key can be imported', async () => { diff --git a/test/peerid.js b/test/peerid.js index 42274db0..4360e538 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -10,7 +10,6 @@ const multihash = require('multihashes') const crypto = require('libp2p-crypto') const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') const rsaClass = require('libp2p-crypto/src/keys/rsa-class') -const promisify = require('promisify-es6') const sample = { id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', @@ -24,7 +23,7 @@ describe('peer ID', () => { before(async () => { const encoded = Buffer.from(sample.privKey, 'base64') - peer = await promisify(PeerId.createFromPrivKey)(encoded) + peer = await PeerId.createFromPrivKey(encoded) }) it('decoded public key', async () => { @@ -35,18 +34,14 @@ describe('peer ID', () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await promisify(crypto.keys.unmarshalPrivateKey, { - context: crypto.keys - })(privateKeyProtobuf) + const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) expect(key).to.exist() }) it('encoded public key with DER', async () => { const jwk = rsaUtils.pkixToJwk(publicKeyDer) const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await promisify(rsa.hash, { - context: rsa - })() + const keyId = await rsa.hash() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -60,9 +55,7 @@ describe('peer ID', () => { kid: '2011-04-29' } const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await promisify(rsa.hash, { - context: rsa - })() + const keyId = await rsa.hash() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -70,9 +63,7 @@ describe('peer ID', () => { it('decoded private key', async () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await promisify(crypto.keys.unmarshalPrivateKey, { - context: crypto.keys - })(privateKeyProtobuf) + const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) expect(key).to.exist() }) }) From 8ff68d1c502e4b46264fc9ba1380e0c34c96c4dd Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 10:13:05 +0000 Subject: [PATCH 79/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff6dbc6d..fe630675 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.5.2", + "version": "0.5.3", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From be63323cef833672b585f4bab93e923138791179 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 10:13:05 +0000 Subject: [PATCH 80/87] chore: release version v0.5.3 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd36aa07..6a11dbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18) + + + ## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02) From 6b9516cb3c56c836f207fd267e561727b4710b31 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 16:46:28 +0000 Subject: [PATCH 81/87] Revert "chore: update deps (#40)" This reverts commit b6d5313a550555b435bd301d811af75397dc91e4. --- package.json | 14 +++++++------- src/keychain.js | 42 ++++++++++++++++++++++++++++++++---------- test/keychain.spec.js | 3 ++- test/peerid.js | 19 ++++++++++++++----- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index fe630675..56359fec 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "err-code": "^2.0.0", - "interface-datastore": "^0.8.0", - "libp2p-crypto": "^0.17.1", - "merge-options": "^2.0.0", + "interface-datastore": "^0.7.0", + "libp2p-crypto": "^0.16.2", + "merge-options": "^1.0.1", "node-forge": "^0.9.1", "sanitize-filename": "^1.6.1" }, @@ -52,13 +52,13 @@ "chai": "^4.2.0", "chai-string": "^1.5.0", "datastore-fs": "^0.9.0", - "datastore-level": "^0.14.0", + "datastore-level": "^0.12.1", "dirty-chai": "^2.0.1", - "level": "^6.0.0", + "level": "^5.0.1", "multihashes": "^0.4.15", - "peer-id": "^0.13.5", + "peer-id": "^0.12.2", "promisify-es6": "^1.0.3", - "rimraf": "^3.0.0" + "rimraf": "^2.6.3" }, "contributors": [ "Alan Shaw ", diff --git a/src/keychain.js b/src/keychain.js index aae78972..7bd8ba34 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,6 +5,7 @@ const sanitize = require('sanitize-filename') const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') +const promisify = require('promisify-es6') const CMS = require('./cms') const errcode = require('err-code') @@ -205,9 +206,16 @@ class Keychain { let keyInfo try { - const keypair = await crypto.keys.generateKeyPair(type, size) - const kid = await keypair.id() - const pem = await keypair.export(this._()) + const keypair = await promisify(crypto.keys.generateKeyPair, { + context: crypto.keys + })(type, size) + + const kid = await promisify(keypair.id, { + context: keypair + })() + const pem = await promisify(keypair.export, { + context: keypair + })(this._()) keyInfo = { name: name, id: kid @@ -359,8 +367,12 @@ class Keychain { try { const res = await this.store.get(dsname) const pem = res.toString() - const privateKey = await crypto.keys.import(pem, this._()) - return privateKey.export(password) + const privateKey = await promisify(crypto.keys.import, { + context: crypto.keys + })(pem, this._()) + return promisify(privateKey.export, { + context: privateKey + })(password) } catch (err) { return throwDelayed(err) } @@ -388,15 +400,21 @@ class Keychain { let privateKey try { - privateKey = await crypto.keys.import(pem, password) + privateKey = await promisify(crypto.keys.import, { + context: crypto.keys + })(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._()) + kid = await promisify(privateKey.id, { + context: privateKey + })() + pem = await promisify(privateKey.export, { + context: privateKey + })(this._()) } catch (err) { return throwDelayed(err) } @@ -428,8 +446,12 @@ class Keychain { 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 kid = await promisify(privateKey.id, { + context: privateKey + })() + const pem = await promisify(privateKey.export, { + context: privateKey + })(this._()) const keyInfo = { name: name, id: kid diff --git a/test/keychain.spec.js b/test/keychain.spec.js index c455f2d7..0756f5f2 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -9,6 +9,7 @@ chai.use(require('dirty-chai')) chai.use(require('chai-string')) const Keychain = require('../') const PeerId = require('peer-id') +const promisify = require('promisify-es6') module.exports = (datastore1, datastore2) => { describe('keychain', () => { @@ -268,7 +269,7 @@ module.exports = (datastore1, datastore2) => { before(async function () { const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await PeerId.createFromPrivKey(encoded) + alice = await promisify(PeerId.createFromPrivKey)(encoded) }) it('private key can be imported', async () => { diff --git a/test/peerid.js b/test/peerid.js index 4360e538..42274db0 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -10,6 +10,7 @@ const multihash = require('multihashes') const crypto = require('libp2p-crypto') const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') const rsaClass = require('libp2p-crypto/src/keys/rsa-class') +const promisify = require('promisify-es6') const sample = { id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', @@ -23,7 +24,7 @@ describe('peer ID', () => { before(async () => { const encoded = Buffer.from(sample.privKey, 'base64') - peer = await PeerId.createFromPrivKey(encoded) + peer = await promisify(PeerId.createFromPrivKey)(encoded) }) it('decoded public key', async () => { @@ -34,14 +35,18 @@ describe('peer ID', () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + const key = await promisify(crypto.keys.unmarshalPrivateKey, { + context: crypto.keys + })(privateKeyProtobuf) expect(key).to.exist() }) it('encoded public key with DER', async () => { const jwk = rsaUtils.pkixToJwk(publicKeyDer) const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await rsa.hash() + const keyId = await promisify(rsa.hash, { + context: rsa + })() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -55,7 +60,9 @@ describe('peer ID', () => { kid: '2011-04-29' } const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await rsa.hash() + const keyId = await promisify(rsa.hash, { + context: rsa + })() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -63,7 +70,9 @@ describe('peer ID', () => { it('decoded private key', async () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) + const key = await promisify(crypto.keys.unmarshalPrivateKey, { + context: crypto.keys + })(privateKeyProtobuf) expect(key).to.exist() }) }) From 66c1fb37b617578bb7f9512d774629fe3b5c0572 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 16:52:29 +0000 Subject: [PATCH 82/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56359fec..a3e7fbdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.5.3", + "version": "0.5.4", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From 0d13a8b729e22e3a30084296f900e11026dcdc24 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 16:52:29 +0000 Subject: [PATCH 83/87] chore: release version v0.5.4 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a11dbf9..71becfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18) + + + ## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18) From 464fcbeddf599f9dc561a451fb57be8a022be657 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 18 Dec 2019 10:04:20 +0000 Subject: [PATCH 84/87] chore: update deps (#40) --- package.json | 14 +++++++------- src/keychain.js | 42 ++++++++++-------------------------------- test/keychain.spec.js | 3 +-- test/peerid.js | 19 +++++-------------- 4 files changed, 23 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index a3e7fbdc..90b6f34c 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", "dependencies": { "err-code": "^2.0.0", - "interface-datastore": "^0.7.0", - "libp2p-crypto": "^0.16.2", - "merge-options": "^1.0.1", + "interface-datastore": "^0.8.0", + "libp2p-crypto": "^0.17.1", + "merge-options": "^2.0.0", "node-forge": "^0.9.1", "sanitize-filename": "^1.6.1" }, @@ -52,13 +52,13 @@ "chai": "^4.2.0", "chai-string": "^1.5.0", "datastore-fs": "^0.9.0", - "datastore-level": "^0.12.1", + "datastore-level": "^0.14.0", "dirty-chai": "^2.0.1", - "level": "^5.0.1", + "level": "^6.0.0", "multihashes": "^0.4.15", - "peer-id": "^0.12.2", + "peer-id": "^0.13.5", "promisify-es6": "^1.0.3", - "rimraf": "^2.6.3" + "rimraf": "^3.0.0" }, "contributors": [ "Alan Shaw ", diff --git a/src/keychain.js b/src/keychain.js index 7bd8ba34..aae78972 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,7 +5,6 @@ const sanitize = require('sanitize-filename') const mergeOptions = require('merge-options') const crypto = require('libp2p-crypto') const DS = require('interface-datastore') -const promisify = require('promisify-es6') const CMS = require('./cms') const errcode = require('err-code') @@ -206,16 +205,9 @@ class Keychain { let keyInfo try { - const keypair = await promisify(crypto.keys.generateKeyPair, { - context: crypto.keys - })(type, size) - - const kid = await promisify(keypair.id, { - context: keypair - })() - const pem = await promisify(keypair.export, { - context: keypair - })(this._()) + const keypair = await crypto.keys.generateKeyPair(type, size) + const kid = await keypair.id() + const pem = await keypair.export(this._()) keyInfo = { name: name, id: kid @@ -367,12 +359,8 @@ class Keychain { try { const res = await this.store.get(dsname) const pem = res.toString() - const privateKey = await promisify(crypto.keys.import, { - context: crypto.keys - })(pem, this._()) - return promisify(privateKey.export, { - context: privateKey - })(password) + const privateKey = await crypto.keys.import(pem, this._()) + return privateKey.export(password) } catch (err) { return throwDelayed(err) } @@ -400,21 +388,15 @@ class Keychain { let privateKey try { - privateKey = await promisify(crypto.keys.import, { - context: crypto.keys - })(pem, password) + 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 promisify(privateKey.id, { - context: privateKey - })() - pem = await promisify(privateKey.export, { - context: privateKey - })(this._()) + kid = await privateKey.id() + pem = await privateKey.export(this._()) } catch (err) { return throwDelayed(err) } @@ -446,12 +428,8 @@ class Keychain { if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS')) try { - const kid = await promisify(privateKey.id, { - context: privateKey - })() - const pem = await promisify(privateKey.export, { - context: privateKey - })(this._()) + const kid = await privateKey.id() + const pem = await privateKey.export(this._()) const keyInfo = { name: name, id: kid diff --git a/test/keychain.spec.js b/test/keychain.spec.js index 0756f5f2..c455f2d7 100644 --- a/test/keychain.spec.js +++ b/test/keychain.spec.js @@ -9,7 +9,6 @@ chai.use(require('dirty-chai')) chai.use(require('chai-string')) const Keychain = require('../') const PeerId = require('peer-id') -const promisify = require('promisify-es6') module.exports = (datastore1, datastore2) => { describe('keychain', () => { @@ -269,7 +268,7 @@ module.exports = (datastore1, datastore2) => { before(async function () { const encoded = Buffer.from(alicePrivKey, 'base64') - alice = await promisify(PeerId.createFromPrivKey)(encoded) + alice = await PeerId.createFromPrivKey(encoded) }) it('private key can be imported', async () => { diff --git a/test/peerid.js b/test/peerid.js index 42274db0..4360e538 100644 --- a/test/peerid.js +++ b/test/peerid.js @@ -10,7 +10,6 @@ const multihash = require('multihashes') const crypto = require('libp2p-crypto') const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils') const rsaClass = require('libp2p-crypto/src/keys/rsa-class') -const promisify = require('promisify-es6') const sample = { id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', @@ -24,7 +23,7 @@ describe('peer ID', () => { before(async () => { const encoded = Buffer.from(sample.privKey, 'base64') - peer = await promisify(PeerId.createFromPrivKey)(encoded) + peer = await PeerId.createFromPrivKey(encoded) }) it('decoded public key', async () => { @@ -35,18 +34,14 @@ describe('peer ID', () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await promisify(crypto.keys.unmarshalPrivateKey, { - context: crypto.keys - })(privateKeyProtobuf) + const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) expect(key).to.exist() }) it('encoded public key with DER', async () => { const jwk = rsaUtils.pkixToJwk(publicKeyDer) const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await promisify(rsa.hash, { - context: rsa - })() + const keyId = await rsa.hash() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -60,9 +55,7 @@ describe('peer ID', () => { kid: '2011-04-29' } const rsa = new rsaClass.RsaPublicKey(jwk) - const keyId = await promisify(rsa.hash, { - context: rsa - })() + const keyId = await rsa.hash() const kids = multihash.toB58String(keyId) expect(kids).to.equal(peer.toB58String()) }) @@ -70,9 +63,7 @@ describe('peer ID', () => { it('decoded private key', async () => { // get protobuf version of the private key const privateKeyProtobuf = peer.marshalPrivKey() - const key = await promisify(crypto.keys.unmarshalPrivateKey, { - context: crypto.keys - })(privateKeyProtobuf) + const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf) expect(key).to.exist() }) }) From 24e10f378b0c53df198149c1f9e9d8d4cb90b07a Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 16:58:32 +0000 Subject: [PATCH 85/87] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90b6f34c..29b8873a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-keychain", - "version": "0.5.4", + "version": "0.6.0", "description": "Key management and cryptographically protected messages", "leadMaintainer": "Vasco Santos ", "main": "src/index.js", From 44a1e7c709e71aae37f968305340d9ccfbd4ee49 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 16:58:32 +0000 Subject: [PATCH 86/87] chore: release version v0.6.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71becfb4..f661d419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +# [0.6.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.4...v0.6.0) (2019-12-18) + + + ## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18) From 7e1a49f0aa51b3f6555673c897a54e498b638b32 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 23 Dec 2019 11:30:45 +0000 Subject: [PATCH 87/87] chore: move keychain to libp2p --- .gitignore | 45 ----------------------- .travis.yml | 42 --------------------- CHANGELOG.md => keychain/CHANGELOG.md | 0 LICENSE => keychain/LICENSE | 0 README.md => keychain/README.md | 0 {doc => keychain/doc}/private-key.png | Bin {doc => keychain/doc}/private-key.xml | 0 package.json => keychain/package.json | 0 {src => keychain/src}/cms.js | 0 {src => keychain/src}/index.js | 0 {src => keychain/src}/keychain.js | 0 {src => keychain/src}/util.js | 0 {test => keychain/test}/browser.js | 0 {test => keychain/test}/cms-interop.js | 0 {test => keychain/test}/keychain.spec.js | 0 {test => keychain/test}/node.js | 0 {test => keychain/test}/peerid.js | 0 17 files changed, 87 deletions(-) delete mode 100644 .gitignore delete mode 100644 .travis.yml rename CHANGELOG.md => keychain/CHANGELOG.md (100%) rename LICENSE => keychain/LICENSE (100%) rename README.md => keychain/README.md (100%) rename {doc => keychain/doc}/private-key.png (100%) rename {doc => keychain/doc}/private-key.xml (100%) rename package.json => keychain/package.json (100%) rename {src => keychain/src}/cms.js (100%) rename {src => keychain/src}/index.js (100%) rename {src => keychain/src}/keychain.js (100%) rename {src => keychain/src}/util.js (100%) rename {test => keychain/test}/browser.js (100%) rename {test => keychain/test}/cms-interop.js (100%) rename {test => keychain/test}/keychain.spec.js (100%) rename {test => keychain/test}/node.js (100%) rename {test => keychain/test}/peerid.js (100%) diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3da57e81..00000000 --- a/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -docs -**/node_modules/ -**/*.log -test/repo-tests* -**/bundle.js - -# Logs -logs -*.log - -coverage -.nyc_output - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -build - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules - -lib -dist -test/test-data/go-ipfs-repo/LOCK -test/test-data/go-ipfs-repo/LOG -test/test-data/go-ipfs-repo/LOG.old - -# while testing npm5 -package-lock.json -yarn.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2061bd32..00000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: node_js -cache: npm -stages: - - check - - test - - cov - -node_js: - - '10' - - '12' - -os: - - linux - - osx - - windows - -script: npx nyc -s npm run test:node -- --bail -after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov - -jobs: - include: - - stage: check - script: - - npx aegir dep-check - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: - - npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: - - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - -notifications: - email: false diff --git a/CHANGELOG.md b/keychain/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to keychain/CHANGELOG.md diff --git a/LICENSE b/keychain/LICENSE similarity index 100% rename from LICENSE rename to keychain/LICENSE diff --git a/README.md b/keychain/README.md similarity index 100% rename from README.md rename to keychain/README.md diff --git a/doc/private-key.png b/keychain/doc/private-key.png similarity index 100% rename from doc/private-key.png rename to keychain/doc/private-key.png diff --git a/doc/private-key.xml b/keychain/doc/private-key.xml similarity index 100% rename from doc/private-key.xml rename to keychain/doc/private-key.xml diff --git a/package.json b/keychain/package.json similarity index 100% rename from package.json rename to keychain/package.json diff --git a/src/cms.js b/keychain/src/cms.js similarity index 100% rename from src/cms.js rename to keychain/src/cms.js diff --git a/src/index.js b/keychain/src/index.js similarity index 100% rename from src/index.js rename to keychain/src/index.js diff --git a/src/keychain.js b/keychain/src/keychain.js similarity index 100% rename from src/keychain.js rename to keychain/src/keychain.js diff --git a/src/util.js b/keychain/src/util.js similarity index 100% rename from src/util.js rename to keychain/src/util.js diff --git a/test/browser.js b/keychain/test/browser.js similarity index 100% rename from test/browser.js rename to keychain/test/browser.js diff --git a/test/cms-interop.js b/keychain/test/cms-interop.js similarity index 100% rename from test/cms-interop.js rename to keychain/test/cms-interop.js diff --git a/test/keychain.spec.js b/keychain/test/keychain.spec.js similarity index 100% rename from test/keychain.spec.js rename to keychain/test/keychain.spec.js diff --git a/test/node.js b/keychain/test/node.js similarity index 100% rename from test/node.js rename to keychain/test/node.js diff --git a/test/peerid.js b/keychain/test/peerid.js similarity index 100% rename from test/peerid.js rename to keychain/test/peerid.js