chore: integrate libp2p-keychain into js-libp2p (#633)

Integrates the libp2p-keychain codebase into this repo

Co-authored-by: David Dias <daviddias.p@gmail.com>
Co-authored-by: Richard Schneider <makaretu@gmail.com>
Co-authored-by: Maciej Krüger <mkg20001@gmail.com>
Co-authored-by: Victor Bjelkholm <victorbjelkholm@gmail.com>
Co-authored-by: Masahiro Saito <camelmasa@gmail.com>
Co-authored-by: Alan Shaw <alan.shaw@protocol.ai>
Co-authored-by: Hugo Dias <mail@hugodias.me>
Co-authored-by: Alberto Elias <hi@albertoelias.me>
Co-authored-by: Alex Potsides <alex@achingbrain.net>
Co-authored-by: Jacob Heun <jacobheun@gmail.com>
This commit is contained in:
Vasco Santos 2020-05-12 14:09:31 +02:00 committed by Jacob Heun
parent 6627278a87
commit 8bf5a70bb8
13 changed files with 1393 additions and 1 deletions

View File

@ -67,12 +67,14 @@
"multiaddr": "^7.4.3",
"multistream-select": "^0.15.0",
"mutable-proxy": "^1.0.0",
"node-forge": "^0.9.1",
"p-any": "^3.0.0",
"p-fifo": "^1.0.0",
"p-settle": "^4.0.1",
"peer-id": "^0.13.11",
"protons": "^1.0.1",
"retimer": "^2.0.0",
"sanitize-filename": "^1.6.3",
"streaming-iterables": "^4.1.0",
"timeout-abort-controller": "^1.0.0",
"xsalsa20": "^1.0.2"
@ -84,7 +86,10 @@
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-bytes": "^0.1.2",
"chai-string": "^1.5.0",
"cids": "^0.8.0",
"datastore-fs": "^1.1.0",
"datastore-level": "^1.1.0",
"delay": "^4.3.0",
"dirty-chai": "^2.0.1",
"interop-libp2p": "libp2p/interop#chore/update-libp2p-daemon-with-peerstore",
@ -92,6 +97,7 @@
"it-concat": "^1.0.0",
"it-pair": "^1.0.0",
"it-pushable": "^1.4.0",
"level": "^6.0.1",
"libp2p-bootstrap": "^0.11.0",
"libp2p-delegated-content-routing": "^0.5.0",
"libp2p-delegated-peer-routing": "^0.5.0",
@ -99,16 +105,19 @@
"libp2p-gossipsub": "^0.4.0",
"libp2p-kad-dht": "^0.19.1",
"libp2p-mdns": "^0.14.1",
"libp2p-noise": "^1.1.0",
"libp2p-mplex": "^0.9.5",
"libp2p-noise": "^1.1.0",
"libp2p-secio": "^0.12.4",
"libp2p-tcp": "^0.14.1",
"libp2p-webrtc-star": "^0.18.0",
"libp2p-websockets": "^0.13.1",
"multihashes": "^0.4.19",
"nock": "^12.0.3",
"p-defer": "^3.0.0",
"p-times": "^3.0.0",
"p-wait-for": "^3.1.0",
"promisify-es6": "^1.0.3",
"rimraf": "^3.0.2",
"sinon": "^9.0.2"
},
"contributors": [

123
src/keychain/README.md Normal file
View File

@ -0,0 +1,123 @@
# js-libp2p-keychain
[![](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)
[![](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)
> A secure key chain for libp2p in JavaScript
## Lead Maintainer
[Vasco Santos](https://github.com/vasco-santos).
## 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
```sh
npm install --save libp2p-keychain
```
### Usage
```js
const Keychain = require('libp2p-keychain')
const FsStore = require('datastore-fs')
const datastore = new FsStore('./a-keystore')
const opts = {
passPhrase: 'some long easily remembered phrase'
}
const keychain = new Keychain(datastore, opts)
```
## API
Managing a key
- `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
- `async listKeys ()`
- `async findKeyById (id)`
- `async findKeyByName (name)`
Cryptographically protected messages
- `async cms.encrypt (name, plain)`
- `async cms.decrypt (cmsData)`
### 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.
```js
{
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**.
The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function.
```js
const defaultOptions = {
//See https://cryptosense.com/parameter-choice-for-pbkdf2/
dek: {
keyLength: 512 / 8,
iterationCount: 1000,
salt: 'at least 16 characters long',
hash: 'sha2-512'
}
}
```
![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.
### Cryptographic Message Syntax (CMS)
CMS, aka [PKCS #7](https://en.wikipedia.org/wiki/PKCS) and [RFC 5652](https://tools.ietf.org/html/rfc5652), describes an encapsulation syntax for data protection. It is used to digitally sign, digest, authenticate, or encrypt arbitrary message content. Basically, `cms.encrypt` creates a DER message that can be only be read by someone holding the private key.
## Contribute
Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-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)
## License
[MIT](LICENSE)

122
src/keychain/cms.js Normal file
View File

@ -0,0 +1,122 @@
'use strict'
require('node-forge/lib/pkcs7')
require('node-forge/lib/pbe')
const forge = require('node-forge/lib/forge')
const { certificateForKey, findAsync } = require('./util')
const errcode = require('err-code')
/**
* Cryptographic Message Syntax (aka PKCS #7)
*
* CMS describes an encapsulation syntax for data protection. It
* is used to digitally sign, digest, authenticate, or encrypt
* arbitrary message content.
*
* See RFC 5652 for all the details.
*/
class CMS {
/**
* Creates a new instance with a keychain
*
* @param {Keychain} keychain - the available keys
*/
constructor (keychain) {
if (!keychain) {
throw errcode(new Error('keychain is required'), 'ERR_KEYCHAIN_REQUIRED')
}
this.keychain = keychain
}
/**
* Creates some protected data.
*
* The output Buffer contains the PKCS #7 message in DER.
*
* @param {string} name - The local key name.
* @param {Buffer} plain - The data to encrypt.
* @returns {undefined}
*/
async encrypt (name, plain) {
if (!Buffer.isBuffer(plain)) {
throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')
}
const key = await this.keychain.findKeyByName(name)
const pem = await this.keychain._getPrivateKey(name)
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
const certificate = await certificateForKey(key, privateKey)
// create a p7 enveloped message
const p7 = forge.pkcs7.createEnvelopedData()
p7.addRecipient(certificate)
p7.content = forge.util.createBuffer(plain)
p7.encrypt()
// convert message to DER
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
return Buffer.from(der, 'binary')
}
/**
* Reads some protected data.
*
* The keychain must contain one of the keys used to encrypt the data. If none of the keys
* exists, an Error is returned with the property 'missingKeys'. It is array of key ids.
*
* @param {Buffer} cmsData - The CMS encrypted data to decrypt.
* @returns {undefined}
*/
async decrypt (cmsData) {
if (!Buffer.isBuffer(cmsData)) {
throw errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS')
}
let cms
try {
const buf = forge.util.createBuffer(cmsData.toString('binary'))
const obj = forge.asn1.fromDer(buf)
cms = forge.pkcs7.messageFromAsn1(obj)
} catch (err) {
throw errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS')
}
// 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
}
})
const r = await findAsync(recipients, async (recipient) => {
try {
const key = await this.keychain.findKeyById(recipient.keyId)
if (key) return true
} catch (err) {
return false
}
return false
})
if (!r) {
const missingKeys = recipients.map(r => r.keyId)
throw errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', {
missingKeys
})
}
const key = await this.keychain.findKeyById(r.keyId)
const pem = await this.keychain._getPrivateKey(key.name)
const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
cms.decrypt(r.recipient, privateKey)
return Buffer.from(cms.content.getBytes(), 'binary')
}
}
module.exports = CMS

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

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>

3
src/keychain/index.js Normal file
View File

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

469
src/keychain/keychain.js Normal file
View File

@ -0,0 +1,469 @@
/* eslint max-nested-callbacks: ["error", 5] */
'use strict'
const sanitize = require('sanitize-filename')
const mergeOptions = require('merge-options')
const crypto = require('libp2p-crypto')
const DS = require('interface-datastore')
const CMS = require('./cms')
const errcode = require('err-code')
const keyPrefix = '/pkcs8/'
const infoPrefix = '/info/'
// 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: 'sha2-512'
}
}
function validateKeyName (name) {
if (!name) return false
if (typeof name !== 'string') return false
return name === sanitize(name.trim())
}
/**
* 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 {string | Error} err - The error
* @private
*/
async function throwDelayed (err) {
const min = 200
const max = 1000
const delay = Math.random() * (max - min) + min
await new Promise(resolve => setTimeout(resolve, delay))
throw err
}
/**
* Converts a key name into a datastore name.
*
* @param {string} name
* @returns {DS.Key}
* @private
*/
function DsName (name) {
return new DS.Key(keyPrefix + name)
}
/**
* Converts a key name into a datastore info name.
*
* @param {string} name
* @returns {DS.Key}
* @private
*/
function DsInfoName (name) {
return new DS.Key(infoPrefix + name)
}
/**
* Information about a key.
*
* @typedef {Object} KeyInfo
*
* @property {string} id - The universally unique key id.
* @property {string} name - The local key name.
*/
/**
* Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
*
* A key in the store has two entries
* - '/info/*key-name*', contains the KeyInfo for the key
* - '/pkcs8/*key-name*', contains the PKCS #8 for the key
*
*/
class Keychain {
/**
* Creates a new instance of a key chain.
*
* @param {DS} store - where the key are.
* @param {object} options - ???
*/
constructor (store, options) {
if (!store) {
throw new Error('store is required')
}
this.store = store
const opts = mergeOptions(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}`)
}
// Create the derived encrypting key
const dek = crypto.pbkdf2(
opts.passPhrase,
opts.dek.salt,
opts.dek.iterationCount,
opts.dek.keyLength,
opts.dek.hash)
Object.defineProperty(this, '_', { value: () => dek })
}
/**
* Gets an object that can encrypt/decrypt protected data
* using the Cryptographic Message Syntax (CMS).
*
* CMS describes an encapsulation syntax for data protection. It
* is used to digitally sign, digest, authenticate, or encrypt
* arbitrary message content.
*
* @returns {CMS}
*/
get cms () {
return new CMS(this)
}
/**
* Generates the options for a keychain. A random salt is produced.
*
* @returns {object}
*/
static generateOptions () {
const options = Object.assign({}, defaultOptions)
const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding
options.dek.salt = crypto.randomBytes(saltLength).toString('base64')
return options
}
/**
* Gets an object that can encrypt/decrypt protected data.
* The default options for a keychain.
*
* @returns {object}
*/
static get options () {
return defaultOptions
}
/**
* Create a new key.
*
* @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.
* @returns {KeyInfo}
*/
async createKey (name, type, size) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
if (typeof type !== 'string') {
return throwDelayed(errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE'))
}
if (!Number.isSafeInteger(size)) {
return throwDelayed(errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE'))
}
const dsname = DsName(name)
const exists = await self.store.has(dsname)
if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
switch (type.toLowerCase()) {
case 'rsa':
if (size < 2048) {
return throwDelayed(errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE'))
}
break
default:
break
}
let keyInfo
try {
const keypair = await crypto.keys.generateKeyPair(type, size)
const kid = await keypair.id()
const pem = await keypair.export(this._())
keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
await batch.commit()
} catch (err) {
return throwDelayed(err)
}
return keyInfo
}
/**
* List all the keys.
*
* @returns {KeyInfo[]}
*/
async listKeys () {
const self = this
const query = {
prefix: infoPrefix
}
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.
* @returns {KeyInfo}
*/
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.
* @returns {KeyInfo}
*/
async findKeyByName (name) {
if (!validateKeyName(name)) {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
const dsname = DsInfoName(name)
try {
const res = await this.store.get(dsname)
return JSON.parse(res.toString())
} catch (err) {
return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
}
}
/**
* Remove an existing key.
*
* @param {string} name - The local key name; must already exist.
* @returns {KeyInfo}
*/
async removeKey (name) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
const dsname = DsName(name)
const keyInfo = await self.findKeyByName(name)
const batch = self.store.batch()
batch.delete(dsname)
batch.delete(DsInfoName(name))
await batch.commit()
return keyInfo
}
/**
* Rename a key
*
* @param {string} oldName - The old local key name; must already exist.
* @param {string} newName - The new local key name; must not already exist.
* @returns {KeyInfo}
*/
async renameKey (oldName, newName) {
const self = this
if (!validateKeyName(oldName) || oldName === 'self') {
return throwDelayed(errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID'))
}
if (!validateKeyName(newName) || newName === 'self') {
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)
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
const batch = self.store.batch()
batch.put(newDsname, pem)
batch.put(newInfoName, JSON.stringify(keyInfo))
batch.delete(oldDsname)
batch.delete(oldInfoName)
await batch.commit()
return keyInfo
} catch (err) {
return throwDelayed(err)
}
}
/**
* Export an existing key as a PEM encrypted PKCS #8 string
*
* @param {string} name - The local key name; must already exist.
* @param {string} password - The password
* @returns {string}
*/
async exportKey (name, password) {
if (!validateKeyName(name)) {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
if (!password) {
return throwDelayed(errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED'))
}
const dsname = DsName(name)
try {
const res = await this.store.get(dsname)
const pem = res.toString()
const privateKey = await crypto.keys.import(pem, this._())
return privateKey.export(password)
} catch (err) {
return throwDelayed(err)
}
}
/**
* Import a new key from a PEM encoded PKCS #8 string
*
* @param {string} name - The local key name; must not already exist.
* @param {string} pem - The PEM encoded PKCS #8 string
* @param {string} password - The password.
* @returns {KeyInfo}
*/
async importKey (name, pem, password) {
const self = this
if (!validateKeyName(name) || name === 'self') {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
if (!pem) {
return throwDelayed(errcode(new Error('PEM encoded key is required'), 'ERR_PEM_REQUIRED'))
}
const dsname = DsName(name)
const exists = await self.store.has(dsname)
if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
let privateKey
try {
privateKey = await crypto.keys.import(pem, password)
} catch (err) {
return throwDelayed(errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY'))
}
let kid
try {
kid = await privateKey.id()
pem = await privateKey.export(this._())
} catch (err) {
return throwDelayed(err)
}
const keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
await batch.commit()
return keyInfo
}
async importPeer (name, peer) {
const self = this
if (!validateKeyName(name)) {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
if (!peer || !peer.privKey) {
return throwDelayed(errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY'))
}
const privateKey = peer.privKey
const dsname = DsName(name)
const exists = await self.store.has(dsname)
if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
try {
const kid = await privateKey.id()
const pem = await privateKey.export(this._())
const keyInfo = {
name: name,
id: kid
}
const batch = self.store.batch()
batch.put(dsname, pem)
batch.put(DsInfoName(name), JSON.stringify(keyInfo))
await batch.commit()
return keyInfo
} catch (err) {
return throwDelayed(err)
}
}
/**
* Gets the private key as PEM encoded PKCS #8 string.
*
* @param {string} name
* @returns {string}
* @private
*/
async _getPrivateKey (name) {
if (!validateKeyName(name)) {
return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
}
try {
const dsname = DsName(name)
const res = await this.store.get(dsname)
return res.toString()
} catch (err) {
return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
}
}
}
module.exports = Keychain

89
src/keychain/util.js Normal file
View File

@ -0,0 +1,89 @@
'use strict'
require('node-forge/lib/x509')
const forge = require('node-forge/lib/forge')
const pki = forge.pki
exports = module.exports
/**
* Gets a self-signed X.509 certificate for the key.
*
* The output Buffer contains the PKCS #7 message in DER.
*
* TODO: move to libp2p-crypto package
*
* @param {KeyInfo} key - The id and name of the key
* @param {RsaPrivateKey} privateKey - The naked key
* @returns {undefined}
*/
exports.certificateForKey = (key, privateKey) => {
const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
const cert = pki.createCertificate()
cert.publicKey = publicKey
cert.serialNumber = '01'
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
const attrs = [{
name: 'organizationName',
value: 'ipfs'
}, {
shortName: 'OU',
value: 'keystore'
}, {
name: 'commonName',
value: key.id
}]
cert.setSubject(attrs)
cert.setIssuer(attrs)
cert.setExtensions([{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true
}, {
name: 'nsCertType',
client: true,
server: true,
email: true,
objsign: true,
sslCA: true,
emailCA: true,
objCA: true
}])
// self-sign certificate
cert.sign(privateKey)
return 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

27
test/keychain/browser.js Normal file
View File

@ -0,0 +1,27 @@
/* eslint-env mocha */
'use strict'
const LevelStore = require('datastore-level')
describe('browser', () => {
const datastore1 = new LevelStore('test-keystore-1', { db: require('level') })
const datastore2 = new LevelStore('test-keystore-2', { db: require('level') })
before(() => {
return Promise.all([
datastore1.open(),
datastore2.open()
])
})
after(() => {
return Promise.all([
datastore1.close(),
datastore2.close()
])
})
require('./keychain.spec')(datastore1, datastore2)
require('./cms-interop')(datastore2)
require('./peerid')
})

View File

@ -0,0 +1,66 @@
/* eslint max-nested-callbacks: ["error", 8] */
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const expect = chai.expect
chai.use(dirtyChai)
chai.use(require('chai-string'))
const Keychain = require('../../src/keychain')
module.exports = (datastore) => {
describe('cms interop', () => {
const passPhrase = 'this is not a secure phrase'
const aliceKeyName = 'cms-interop-alice'
let ks
before(() => {
ks = new Keychain(datastore, { passPhrase: passPhrase })
})
const plainData = Buffer.from('This is a message from Alice to Bob')
it('imports openssl key', async function () {
this.timeout(10 * 1000)
const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA'
const alice = `-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA
MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG
QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd
1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7
/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A
CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri
dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA
ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY
zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/
ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt
0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83
GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH
igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m
3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE
cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL
-----END ENCRYPTED PRIVATE KEY-----
`
const key = await ks.importKey(aliceKeyName, alice, 'mypassword')
expect(key.name).to.equal(aliceKeyName)
expect(key.id).to.equal(aliceKid)
})
it('decrypts node-forge example', async () => {
const example = `
MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK
EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI
WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B
AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k
d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO
knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3
DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B
nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N
`
const plain = await ks.cms.decrypt(Buffer.from(example, 'base64'))
expect(plain).to.exist()
expect(plain.toString()).to.equal(plainData.toString())
})
})
}

View File

@ -0,0 +1,383 @@
/* eslint max-nested-callbacks: ["error", 8] */
/* eslint-env mocha */
'use strict'
const chai = require('chai')
const expect = chai.expect
const fail = expect.fail
chai.use(require('dirty-chai'))
chai.use(require('chai-string'))
const Keychain = require('../../src/keychain')
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) => {
ks = new Keychain(datastore2, { passPhrase: passPhrase })
emptyKeystore = new Keychain(datastore1, { 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()
})
it('needs a supported hashing alorithm', () => {
const ok = new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'sha2-256' } })
expect(ok).to.exist()
expect(() => new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'my-hash' } })).to.throw()
})
it('can generate options', () => {
const options = Keychain.generateOptions()
options.passPhrase = passPhrase
const chain = new Keychain(datastore2, options)
expect(chain).to.exist()
})
describe('key name', () => {
it('is a valid filename and non-ASCII', 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', 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', async () => {
const pem = await ks._getPrivateKey(rsaKeyName)
return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
})
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')
})
it('cannot create the "self" key', async () => {
const err = await ks.createKey('self', 'rsa', 2048).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('should validate name is string', async () => {
const err = await ks.createKey(5, 'rsa', 2048).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('should validate type is string', async () => {
const err = await ks.createKey('TEST' + Date.now(), null, 2048).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_TYPE')
})
it('should validate size is integer', async () => {
const err = await ks.createKey('TEST' + Date.now(), 'rsa', 'string').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE')
})
describe('implements NIST SP 800-131A', () => {
it('disallows RSA length < 2048', async () => {
const err = await ks.createKey('bad-nist-rsa', 'rsa', 1024).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE')
})
})
})
describe('query', () => {
it('finds all existing keys', async () => {
const keys = await ks.listKeys()
expect(keys).to.exist()
const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize())
expect(mykey).to.exist()
})
it('finds a key by name', async () => {
const key = await ks.findKeyByName(rsaKeyName)
expect(key).to.exist()
expect(key).to.deep.equal(rsaKeyInfo)
})
it('finds a key by id', async () => {
const key = await ks.findKeyById(rsaKeyInfo.id)
expect(key).to.exist()
expect(key).to.deep.equal(rsaKeyInfo)
})
it('returns the key\'s name and id', 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')
})
})
})
describe('CMS protected data', () => {
const plainData = Buffer.from('This is a message from Alice to Bob')
let cms
it('service is available', () => {
expect(ks).to.have.property('cms')
})
it('requires a key', async () => {
const err = await ks.cms.encrypt('no-key', plainData).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
})
it('requires plain data as a Buffer', async () => {
const err = await ks.cms.encrypt(rsaKeyName, 'plain data').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_PARAMS')
})
it('encrypts', async () => {
cms = await ks.cms.encrypt(rsaKeyName, plainData)
expect(cms).to.exist()
expect(cms).to.be.instanceOf(Buffer)
})
it('is a PKCS #7 message', async () => {
const err = await ks.cms.decrypt('not CMS').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_PARAMS')
})
it('is a PKCS #7 binary message', async () => {
const err = await ks.cms.decrypt(plainData).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_CMS')
})
it('cannot be read without the key', async () => {
const err = await emptyKeystore.cms.decrypt(cms).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('missingKeys')
expect(err.missingKeys).to.eql([rsaKeyInfo.id])
expect(err).to.have.property('code', 'ERR_MISSING_KEYS')
})
it('can be read with the key', async () => {
const plain = await ks.cms.decrypt(cms)
expect(plain).to.exist()
expect(plain.toString()).to.equal(plainData.toString())
})
})
describe('exported key', () => {
let pemKey
it('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('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)
})
it('requires the pem', async () => {
const err = await ks.importKey('imported-key', undefined, 'password').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_PEM_REQUIRED')
})
it('cannot be imported as an existing key name', async () => {
const err = await ks.importKey(rsaKeyName, pemKey, 'password').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
})
it('cannot be imported with the wrong password', async () => {
const err = await ks.importKey('a-new-name-for-import', pemKey, 'not the password').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_CANNOT_READ_KEY')
})
})
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(async function () {
const encoded = Buffer.from(alicePrivKey, 'base64')
alice = await PeerId.createFromPrivKey(encoded)
})
it('private key can be imported', async () => {
const key = await ks.importPeer('alice', alice)
expect(key.name).to.equal('alice')
expect(key.id).to.equal(alice.toB58String())
})
it('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())
})
it('key name exists', async () => {
const key = await ks.findKeyByName('alice')
expect(key).to.exist()
expect(key).to.have.property('name', 'alice')
expect(key).to.have.property('id', alice.toB58String())
})
})
describe('rename', () => {
it('requires an existing key name', async () => {
const err = await ks.renameKey('not-there', renamedRsaKeyName).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_NOT_FOUND')
})
it('requires a valid new key name', async () => {
const err = await ks.renameKey(rsaKeyName, '..\not-valid').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID')
})
it('does not overwrite existing key', async () => {
const err = await ks.renameKey(rsaKeyName, rsaKeyName).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
})
it('cannot create the "self" key', async () => {
const err = await ks.renameKey(rsaKeyName, 'self').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID')
})
it('removes the existing key name', async () => {
const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName)
expect(key).to.exist()
expect(key).to.have.property('name', renamedRsaKeyName)
expect(key).to.have.property('id', rsaKeyInfo.id)
// Try to find the changed key
const err = await ks.findKeyByName(rsaKeyName).then(fail, err => err)
expect(err).to.exist()
})
it('creates the new key name', async () => {
const key = await ks.findKeyByName(renamedRsaKeyName)
expect(key).to.exist()
expect(key).to.have.property('name', renamedRsaKeyName)
})
it('does not change the key ID', async () => {
const key = await ks.findKeyByName(renamedRsaKeyName)
expect(key).to.exist()
expect(key).to.have.property('name', renamedRsaKeyName)
expect(key).to.have.property('id', rsaKeyInfo.id)
})
it('throws with invalid key names', async () => {
const err = await ks.findKeyByName(undefined).then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
})
describe('key removal', () => {
it('cannot remove the "self" key', async () => {
const err = await ks.removeKey('self').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
})
it('cannot remove an unknown key', async () => {
const err = await ks.removeKey('not-there').then(fail, err => err)
expect(err).to.exist()
expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
})
it('can remove a known key', 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)
})
})
})
}

31
test/keychain/node.js Normal file
View File

@ -0,0 +1,31 @@
/* eslint-env mocha */
'use strict'
const os = require('os')
const path = require('path')
const promisify = require('promisify-es6')
const rimraf = promisify(require('rimraf'))
const FsStore = require('datastore-fs')
describe('node', () => {
const store1 = path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())
const store2 = path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())
const datastore1 = new FsStore(store1)
const datastore2 = new FsStore(store2)
before(async () => {
await datastore1.open()
await datastore2.open()
})
after(async () => {
await datastore1.close()
await datastore2.close()
await rimraf(store1)
await rimraf(store2)
})
require('./keychain.spec')(datastore1, datastore2)
require('./cms-interop')(datastore2)
require('./peerid')
})

69
test/keychain/peerid.js Normal file
View File

@ -0,0 +1,69 @@
/* 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(async () => {
const encoded = Buffer.from(sample.privKey, 'base64')
peer = await PeerId.createFromPrivKey(encoded)
})
it('decoded public key', async () => {
// get protobuf version of the public key
const publicKeyProtobuf = peer.marshalPubKey()
const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf)
publicKeyDer = publicKey.marshal()
// get protobuf version of the private key
const privateKeyProtobuf = peer.marshalPrivKey()
const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
expect(key).to.exist()
})
it('encoded public key with DER', async () => {
const jwk = rsaUtils.pkixToJwk(publicKeyDer)
const rsa = new rsaClass.RsaPublicKey(jwk)
const keyId = await rsa.hash()
const kids = multihash.toB58String(keyId)
expect(kids).to.equal(peer.toB58String())
})
it('encoded public key with JWT', async () => {
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'
}
const rsa = new rsaClass.RsaPublicKey(jwk)
const keyId = await rsa.hash()
const kids = multihash.toB58String(keyId)
expect(kids).to.equal(peer.toB58String())
})
it('decoded private key', async () => {
// get protobuf version of the private key
const privateKeyProtobuf = peer.marshalPrivKey()
const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
expect(key).to.exist()
})
})