refactor: use async/await instead of callbacks (#37)

BREAKING CHANGE: The api now uses async/await instead of callbacks.

Co-Authored-By: Vasco Santos <vasco.santos@moxy.studio>
This commit is contained in:
Jacob Heun 2019-08-16 13:12:47 +02:00 committed by Vasco Santos
parent 717112bdf8
commit dda315a9c8
12 changed files with 532 additions and 691 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ logs
*.log
coverage
.nyc_output
# Runtime data
pids

View File

@ -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

View File

@ -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

View File

@ -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": [

View File

@ -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,29 +36,17 @@ class CMS {
*
* @param {string} name - The local key name.
* @param {Buffer} plain - The data to encrypt.
* @param {function(Error, Buffer)} callback
* @returns {undefined}
*/
encrypt (name, plain, callback) {
const self = this
const done = (err, result) => setImmediate(() => callback(err, result))
async encrypt (name, plain) {
if (!Buffer.isBuffer(plain)) {
return done(errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS'))
throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')
}
series([
(cb) => self.keychain.findKeyByName(name, cb),
(cb) => self.keychain._getPrivateKey(name, cb)
], (err, results) => {
if (err) return done(err)
let key = results[0]
let pem = results[1]
try {
const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
util.certificateForKey(key, privateKey, (err, certificate) => {
if (err) return callback(err)
const key = await this.keychain.findKeyByName(name)
const pem = await this.keychain._getPrivateKey(name)
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
const certificate = await certificateForKey(key, privateKey)
// create a p7 enveloped message
const p7 = forge.pkcs7.createEnvelopedData()
@ -72,12 +56,7 @@ class CMS {
// convert message to DER
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
done(null, Buffer.from(der, 'binary'))
})
} catch (err) {
done(err)
}
})
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)
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)
err = errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', {
throw 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._())
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)
done(null, Buffer.from(cms.content.getBytes(), 'binary'))
})
}
)
return Buffer.from(cms.content.getBytes(), 'binary')
}
}

View File

@ -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'))
return throwDelayed(errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE'))
}
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)
const keyInfo = {
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))
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyInfo)
})
})
})
})
})
await batch.commit()
} catch (err) {
return throwDelayed(err)
}
return keyInfo
}
/**
* List all the keys.
*
* @param {function(Error, KeyInfo[])} callback
* @returns {undefined}
* @returns {KeyInfo[]}
*/
listKeys (callback) {
async listKeys () {
const self = this
const query = {
prefix: infoPrefix
}
pull(
self.store.query(query),
collect((err, res) => {
if (err) return _error(callback, err)
const info = res.map(r => JSON.parse(r.value))
callback(null, info)
})
)
const info = []
for await (const value of self.store.query(query)) {
info.push(JSON.parse(value.value))
}
return info
}
/**
* Find a key by it's id.
*
* @param {string} id - The universally unique key identifier.
* @param {function(Error, KeyInfo)} callback
* @returns {undefined}
* @returns {KeyInfo}
*/
findKeyById (id, callback) {
this.listKeys((err, keys) => {
if (err) return _error(callback, err)
const key = keys.find((k) => k.id === id)
callback(null, key)
})
async findKeyById (id) {
try {
const keys = await this.listKeys()
return keys.find((k) => k.id === id)
} catch (err) {
return throwDelayed(err)
}
}
/**
* Find a key by it's name.
*
* @param {string} name - The local key name.
* @param {function(Error, KeyInfo)} callback
* @returns {undefined}
* @returns {KeyInfo}
*/
findKeyByName (name, callback) {
async findKeyByName (name) {
if (!validateKeyName(name)) {
return _error(callback, errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
const dsname = DsInfoName(name)
this.store.get(dsname, (err, res) => {
if (err) {
return _error(callback, errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
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'))
}
callback(null, JSON.parse(res.toString()))
})
}
/**
* 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 keyInfo = await self.findKeyByName(name)
const batch = self.store.batch()
batch.delete(dsname)
batch.delete(DsInfoName(name))
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyinfo)
})
})
await batch.commit()
return keyInfo
}
/**
@ -322,32 +304,28 @@ 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 pem = res.toString()
self.store.has(newDsname, (err, exists) => {
if (err) return _error(callback, err)
if (exists) return _error(callback, errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
self.store.get(oldInfoName, (err, res) => {
if (err) return _error(callback, err)
const exists = await self.store.has(newDsname)
if (exists) return throwDelayed(errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
try {
let res = await this.store.get(oldDsname)
const pem = res.toString()
res = await self.store.get(oldInfoName)
const keyInfo = JSON.parse(res.toString())
keyInfo.name = newName
@ -356,13 +334,11 @@ class Keychain {
batch.put(newInfoName, JSON.stringify(keyInfo))
batch.delete(oldDsname)
batch.delete(oldInfoName)
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyInfo)
})
})
})
})
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,27 +373,35 @@ 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 exists = await self.store.has(dsname)
if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
let privateKey
try {
privateKey = await crypto.keys.import(pem, password)
} catch (err) {
return throwDelayed(errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY'))
}
let kid
try {
kid = await privateKey.id()
pem = await privateKey.export(this._())
} catch (err) {
return throwDelayed(err)
}
const keyInfo = {
name: name,
id: kid
@ -428,36 +409,28 @@ class Keychain {
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()
callback(null, keyInfo)
})
})
})
})
})
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)
try {
const kid = await privateKey.id()
const pem = await privateKey.export(this._())
const keyInfo = {
name: name,
id: kid
@ -465,34 +438,32 @@ class Keychain {
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
batch.commit((err) => {
if (err) return _error(callback, err)
callback(null, keyInfo)
})
})
})
})
await batch.commit()
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'))
}
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'))
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'))
}
callback(null, res.toString())
})
}
}

View File

@ -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

View File

@ -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)

View File

@ -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()
const key = await ks.importKey(aliceKeyName, alice, 'mypassword')
expect(key.name).to.equal(aliceKeyName)
expect(key.id).to.equal(aliceKid)
done()
})
})
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()
const plain = await ks.cms.decrypt(Buffer.from(example, 'base64'))
expect(plain).to.exist()
expect(plain.toString()).to.equal(plainData.toString())
done()
})
})
})
}

View File

@ -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,149 +55,112 @@ 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) => {
it('throws if an invalid private key name is given', async () => {
const err = await ks._getPrivateKey(undefined).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('throws if a private key cant be found', async () => {
const err = await ks._getPrivateKey('not real').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
})
it('does not overwrite existing key', async () => {
const err = await ks.createKey(rsaKeyName, 'rsa', 2048).then(fail, err => err)
expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
done()
})
})
it('cannot create the "self" key', (done) => {
ks.createKey('self', 'rsa', 2048, (err) => {
it('cannot create the "self" key', async () => {
const err = await ks.createKey('self', 'rsa', 2048).then(fail, err => err)
expect(err).to.exist()
done()
})
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('should validate name is string', (done) => {
ks.createKey(5, 'rsa', 2048, (err) => {
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.message).to.contain('Invalid key name')
done()
})
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('should validate type is string', (done) => {
ks.createKey('TEST' + Date.now(), null, 2048, (err) => {
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.message).to.contain('Invalid key type')
done()
})
expect(err).to.have.property('code', 'ERR_INVALID_KEY_TYPE')
})
it('should validate size is integer', (done) => {
ks.createKey('TEST' + Date.now(), 'rsa', 'string', (err) => {
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.message).to.contain('Invalid key size')
done()
})
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) => {
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('message', 'Invalid RSA key size 1024')
expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE')
done()
})
})
})
})
describe('query', () => {
it('finds all existing keys', (done) => {
ks.listKeys((err, keys) => {
expect(err).to.not.exist()
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()
done()
})
})
it('finds a key by name', (done) => {
ks.findKeyByName(rsaKeyName, (err, key) => {
expect(err).to.not.exist()
it('finds a key by name', async () => {
const key = await ks.findKeyByName(rsaKeyName)
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()
it('finds a key by id', async () => {
const key = await ks.findKeyById(rsaKeyInfo.id)
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()
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')
})
done()
})
})
})
@ -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) => {
it('requires a key', async () => {
const err = await ks.cms.encrypt('no-key', plainData).then(fail, err => err)
expect(err).to.exist()
done()
})
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) => {
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()
done()
})
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) => {
it('is a PKCS #7 message', async () => {
const err = await ks.cms.decrypt('not CMS').then(fail, err => err)
expect(err).to.exist()
done()
})
expect(err).to.have.property('code', 'ERR_INVALID_PARAMS')
})
it('is a PKCS #7 binary message', (done) => {
ks.cms.decrypt(plainData, (err) => {
it('is a PKCS #7 binary message', async () => {
const err = await ks.cms.decrypt(plainData).then(fail, err => err)
expect(err).to.exist()
done()
})
expect(err).to.have.property('code', 'ERR_INVALID_CMS')
})
it('cannot be read without the key', (done) => {
emptyKeystore.cms.decrypt(cms, (err, plain) => {
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')
done()
})
})
it('can be read with the key', (done) => {
ks.cms.decrypt(cms, (err, plain) => {
expect(err).to.not.exist()
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())
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('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()
it('requires the key name', async () => {
const err = await ks.exportKey(undefined, 'password').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('is a PKCS #8 encrypted pem', async () => {
pemKey = await ks.exportKey(rsaKeyName, 'password')
expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
})
it('can be imported', async () => {
const key = await ks.importKey('imported-key', pemKey, 'password')
expect(key.name).to.equal('imported-key')
expect(key.id).to.equal(rsaKeyInfo.id)
done()
})
})
it('cannot be imported as an existing key name', (done) => {
ks.importKey(rsaKeyName, pemKey, 'password', (err, key) => {
it('requires the pem', async () => {
const err = await ks.importKey('imported-key', undefined, 'password').then(fail, err => err)
expect(err).to.exist()
done()
})
expect(err).to.have.property('code', 'ERR_PEM_REQUIRED')
})
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) => {
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()
done()
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()
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())
done()
})
})
it('key id exists', (done) => {
ks.findKeyById(alice.toB58String(), (err, key) => {
expect(err).to.not.exist()
it('private key import requires a valid name', async () => {
const err = await ks.importPeer(undefined, alice).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('private key import requires the peer', async () => {
const err = await ks.importPeer('alice').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_MISSING_PRIVATE_KEY')
})
it('key id exists', async () => {
const key = await ks.findKeyById(alice.toB58String())
expect(key).to.exist()
expect(key).to.have.property('name', 'alice')
expect(key).to.have.property('id', alice.toB58String())
done()
})
})
it('key name exists', (done) => {
ks.findKeyByName('alice', (err, key) => {
expect(err).to.not.exist()
it('key name exists', async () => {
const key = await ks.findKeyByName('alice')
expect(key).to.exist()
expect(key).to.have.property('name', 'alice')
expect(key).to.have.property('id', alice.toB58String())
done()
})
})
})
describe('rename', () => {
it('requires an existing key name', (done) => {
ks.renameKey('not-there', renamedRsaKeyName, (err) => {
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_KEY_NOT_FOUND')
done()
})
expect(err).to.have.property('code', 'ERR_NOT_FOUND')
})
it('requires a valid new key name', (done) => {
ks.renameKey(rsaKeyName, '..\not-valid', (err) => {
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')
done()
})
})
it('does not overwrite existing key', (done) => {
ks.renameKey(rsaKeyName, rsaKeyName, (err) => {
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')
done()
})
})
it('cannot create the "self" key', (done) => {
ks.renameKey(rsaKeyName, 'self', (err) => {
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')
done()
})
})
it('removes the existing key name', (done) => {
ks.renameKey(rsaKeyName, renamedRsaKeyName, (err, key) => {
expect(err).to.not.exist()
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)
ks.findKeyByName(rsaKeyName, (err, key) => {
// Try to find the changed key
const err = await ks.findKeyByName(rsaKeyName).then(fail, err => err)
expect(err).to.exist()
done()
})
})
})
it('creates the new key name', (done) => {
ks.findKeyByName(renamedRsaKeyName, (err, key) => {
expect(err).to.not.exist()
it('creates the new key name', async () => {
const key = await ks.findKeyByName(renamedRsaKeyName)
expect(key).to.exist()
expect(key).to.have.property('name', renamedRsaKeyName)
done()
})
})
it('does not change the key ID', (done) => {
ks.findKeyByName(renamedRsaKeyName, (err, key) => {
expect(err).to.not.exist()
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)
done()
})
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) => {
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')
done()
})
})
it('cannot remove an unknown key', (done) => {
ks.removeKey('not-there', (err) => {
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')
done()
})
})
it('can remove a known key', (done) => {
ks.removeKey(renamedRsaKeyName, (err, key) => {
expect(err).to.not.exist()
it('can remove a known key', async () => {
const key = await ks.removeKey(renamedRsaKeyName)
expect(key).to.exist()
expect(key).to.have.property('name', renamedRsaKeyName)
expect(key).to.have.property('id', rsaKeyInfo.id)
done()
})
})
})
})

View File

@ -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)

View File

@ -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 keyId = await rsa.hash()
const kids = multihash.toB58String(keyId)
// console.log('id', kids)
expect(kids).to.equal(peer.toB58String())
done()
})
})
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 keyId = await rsa.hash()
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))
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()
})
})