This commit is contained in:
Richard Schneider
2017-12-06 22:56:09 +13:00
parent 49e6c47c40
commit 1a96ae8cb7
14 changed files with 1178 additions and 9 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.png binary
* crlf=input

View File

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

BIN
doc/private-key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
doc/private-key.xml Normal file
View File

@ -0,0 +1 @@
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" version="7.8.2" editor="www.draw.io"><diagram id="a8b2919f-aefc-d24c-c550-ea0bf34e92af" name="Page-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==</diagram></mxfile>

View File

@ -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 <daviddias@ipfs.io>",
"author": "Richard Schneider <makaretu@gmail.com>",
"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"
}
}

97
src/cms.js Normal file
View File

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

View File

@ -1 +1,3 @@
'use strict'
module.exports = require('./keychain')

362
src/keychain.js Normal file
View File

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

86
src/util.js Normal file
View File

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

30
test/browser.js Normal file
View File

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

View File

@ -1,4 +0,0 @@
/* eslint-env mocha */
'use strict'
it('so much testing', () => {})

356
test/keychain.spec.js Normal file
View File

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

34
test/node.js Normal file
View File

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

105
test/peerid.js Normal file
View File

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