From 1a96ae8cb73396ec7fb64edb9897587697bbe026 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Wed, 6 Dec 2017 22:56:09 +1300 Subject: [PATCH] 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 0000000..ef41d4f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.png binary +* crlf=input diff --git a/README.md b/README.md index 9565850..0f89dcd 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 0000000..51cb8c5 --- /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 35ffcf5..9116749 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 0000000..2f2d9c7 --- /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 ccacec3..2704d62 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 0000000..50a6798 --- /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 0000000..c3cd5a1 --- /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 0000000..a2633be --- /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 c638cf8..0000000 --- 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 0000000..cc1048c --- /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 0000000..b003a7c --- /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 0000000..8d3063c --- /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() + }) + }) + +})