Compare commits

...

94 Commits

Author SHA1 Message Date
6973449809 chore: move keychain to libp2p 2020-02-03 16:50:51 +01:00
5ad5d3706a Merge remote-tracking branch 'keychain/master' into refactor/keychain 2020-02-03 16:26:06 +01:00
7e1a49f0aa chore: move keychain to libp2p 2019-12-23 11:30:45 +00:00
44a1e7c709 chore: release version v0.6.0 2019-12-18 16:58:32 +00:00
24e10f378b chore: update contributors 2019-12-18 16:58:32 +00:00
464fcbeddf chore: update deps (#40) 2019-12-18 16:54:14 +00:00
0d13a8b729 chore: release version v0.5.4 2019-12-18 16:52:29 +00:00
66c1fb37b6 chore: update contributors 2019-12-18 16:52:29 +00:00
6b9516cb3c Revert "chore: update deps (#40)"
This reverts commit b6d5313a55.
2019-12-18 16:46:28 +00:00
be63323cef chore: release version v0.5.3 2019-12-18 10:13:05 +00:00
8ff68d1c50 chore: update contributors 2019-12-18 10:13:05 +00:00
b6d5313a55 chore: update deps (#40) 2019-12-18 10:04:20 +00:00
163edbbe88 chore: release version v0.5.2 2019-12-02 17:17:38 +01:00
ff6bd50350 chore: update contributors 2019-12-02 17:17:38 +01:00
8de96817ed chore: update node-forge dependency (#39) 2019-12-02 17:08:08 +01:00
ce8c412fb6 chore: release version v0.5.1 2019-09-25 12:33:28 +02:00
b9eb9d7b4a chore: update contributors 2019-09-25 12:33:28 +02:00
893a2c975c chore: downgrade peer-id to same version used by libp2p (#38) 2019-09-25 12:19:38 +02:00
ad378174f7 chore: release version v0.5.0 2019-08-16 14:25:02 +01:00
e375c2f1e8 chore: update contributors 2019-08-16 14:25:02 +01:00
dda315a9c8 refactor: use async/await instead of callbacks (#37)
BREAKING CHANGE: The api now uses async/await instead of callbacks.

Co-Authored-By: Vasco Santos <vasco.santos@moxy.studio>
2019-08-16 12:12:47 +01:00
717112bdf8 chore: release version v0.4.2 2019-06-13 14:50:37 +01:00
74cb4d4775 chore: update contributors 2019-06-13 14:50:37 +01:00
7051b9c530 fix: throw errors with correct stack trace (#35)
The stack trace of thrown error objects is created when the object is
instantiated - if we defer to a function to create the error we end
up with misleading stack traces.

This PR instantiates errors where errors occur and also uses the
`err-code` module to add a `.code` property so we don't have to depend
on string error messages for the type of error that was thrown.
2019-06-13 14:35:12 +01:00
ef47374941 chore: add discourse badge (#34) 2019-04-11 11:20:18 +01:00
a5fd967c02 chore: release version v0.4.1 2019-03-14 22:38:45 +00:00
4e4d3d4b6f chore: update contributors 2019-03-14 22:38:45 +00:00
f71a6bbb0a Revert "feat: adds support for ed25199 and secp256k1 (#31)"
This reverts commit 9eb11f4245.
2019-03-14 22:26:07 +00:00
e30330e1a0 chore: release version v0.4.0 2019-02-26 11:39:50 +00:00
267002f646 chore: update contributors 2019-02-26 11:39:49 +00:00
217cfd3de8 chore: update libp2p-crypto (#33) 2019-02-26 11:22:59 +00:00
9eb11f4245 feat: adds support for ed25199 and secp256k1 (#31) 2019-02-25 11:04:54 +00:00
3779bd0ba2 chore: use travis (#32) 2019-02-18 14:15:55 +00:00
aa5a6cb73c chore: release version v0.3.6 2019-01-10 11:33:28 +00:00
eaf6a88b47 chore: update contributors 2019-01-10 11:33:28 +00:00
18357e678f Merge branch 'master' of github.com:libp2p/js-libp2p-keychain 2019-01-10 11:25:24 +00:00
4dd2ad36dd chore: release version v0.3.5 2019-01-10 11:24:15 +00:00
5cbded55d5 chore: update contributors 2019-01-10 11:24:15 +00:00
7eeed87b10 fix: reduce bundle size (#28) 2019-01-10 11:16:03 +00:00
4b895cf46f chore: release version v0.3.4 2019-01-04 10:54:24 +00:00
a753b1c882 chore: update contributors 2019-01-04 10:54:24 +00:00
17268d5fe3 chore: update dependencies (#29) 2019-01-04 10:51:56 +00:00
251e0b87b6 chore: release version v0.3.3 2018-10-25 09:37:22 +01:00
571c81a2be chore: update contributors 2018-10-25 09:37:22 +01:00
24d4374b20 chore: upgrade dependencies (#27) 2018-10-25 09:31:32 +01:00
5d3f489f23 chore: release version v0.3.2 2018-09-18 13:02:43 +01:00
65129bff3b chore: update contributors 2018-09-18 13:02:43 +01:00
8dfaab1af0 fix: validate createKey params properly (#26)
License: MIT
Signed-off-by: Alan Shaw <alan.shaw@protocol.ai>
2018-09-18 12:48:58 +01:00
f95fef4ad2 chore: use lodash main dependency 2018-07-03 16:21:32 +02:00
73d4530c5b chore: update deps 2018-06-30 16:13:49 +02:00
0065b0a49e chore: fix out of date npms (#21) 2018-06-30 00:38:19 +02:00
974c507069 docs: add lead-maintainer
* docs: add lead-maintainer
2018-06-25 15:06:17 +02:00
ee978a54ea chore: release version v0.3.1 2018-01-28 22:36:45 -08:00
486e54b3ac chore: update contributors 2018-01-28 22:36:44 -08:00
5560669fc9 CMS - PKCS #7 (#19)
CMS - PKCS #7
2018-01-28 22:34:55 -08:00
acf48a8efe chore: release version v0.3.0 2018-01-28 22:22:59 -08:00
3816b8207f chore: update contributors 2018-01-28 22:22:59 -08:00
2ce44446a2 fix: deepmerge 2.0.1 fails in browser, stay with 1.5.2 2018-01-29 18:44:51 +13:00
1e276f6e94 chore: update deps 2018-01-28 20:14:57 -08:00
6a84873a0a chore: release version v0.2.1 2017-12-28 08:51:26 +00:00
849a7c75d0 chore: update contributors 2017-12-28 08:51:25 +00:00
89a451c147 feat: generate unique options for a key chain (#20) 2017-12-28 08:48:32 +00:00
de15d129dd chore: release version v0.2.0 2017-12-20 13:52:09 +00:00
21611e437d chore: update contributors 2017-12-20 13:52:09 +00:00
5343b0f2de chore: update deps 2017-12-20 13:50:56 +00:00
c1627a99e7 feat: use libp2p-crypto (#18)
* test: openssl interop is now the responsibility of libp2p-crypto

* feat: use libp2p-crypto, not node-forge, for key management

* fix: use libp2p-crypto.pbkdf, not node-forge

* fix: do not ship CMS

This removes all depencies on node-forge

* test: update dependencies

* test: remove dead code
2017-12-20 13:43:54 +00:00
605d290525 Merge pull request #17 from libp2p/filenames
test: key name comparision
2017-12-20 11:53:47 +13:00
3b7c691724 test(openssl): verify key id 2017-12-17 13:36:30 +13:00
e78b2483ae test: key name comparision 2017-12-17 12:43:54 +13:00
97bf98fc62 Merge pull request #13 from libp2p/ds-keyinfo
Persist the key info in the store
2017-12-17 12:17:45 +13:00
9129d20bcb docs: correct hash name 2017-12-17 11:30:52 +13:00
b4518e0ca8 Merge pull request #15 from libp2p/automatic-ci-script-update
Updating CI files
2017-12-17 11:16:52 +13:00
ee9dbeb011 Updating CI files
This commit updates all CI scripts to the latest version
2017-12-14 18:04:39 +01:00
1b2664a902 refactor: keep the key info in the store 2017-12-11 14:26:48 +13:00
2dd069b05a test: importing openssl keys 2017-12-10 21:21:10 +13:00
06917f7aba fix: lint errors 2017-12-10 17:37:16 +13:00
ff4f656248 fix: lint errors 2017-12-10 17:21:26 +13:00
f71d3a6521 fix: maps an IPFS hash name to its forge equivalent
Fixes #12
2017-12-10 17:19:20 +13:00
3b8d05abb8 docs(keychain): add API documentation 2017-12-09 20:37:00 +13:00
8305d209b2 fix: error message 2017-12-08 14:46:38 +13:00
f49e753801 fix: return info on removed key #10 2017-12-08 14:45:02 +13:00
506e1d7dc3 Merge pull request #4 from libp2p/mkg20001-patch-1
Add syntax highlighting to README
2017-12-07 01:52:05 +13:00
643bcd4eb2 Add syntax highlighting to README 2017-12-06 13:40:12 +01:00
cfdd2f47bf chore: publish coverage report 2017-12-07 01:34:29 +13:00
99780ab38a chore: ci coverage
Fixes #2
2017-12-07 01:22:35 +13:00
358c8c2ea1 test: disable webworker 2017-12-07 01:06:51 +13:00
569f96342e test: temporarily disable webworker tests #3 2017-12-07 00:51:30 +13:00
98ba68ac82 test: needs more time to generate RSA key 2017-12-07 00:24:39 +13:00
7c44c91788 fix: more linting 2017-12-07 00:16:38 +13:00
409a9990cd fix: linting 2017-12-07 00:10:22 +13:00
658a4d7907 docs: install and links 2017-12-06 23:13:02 +13:00
1a96ae8cb7 feat: move bits from https://github.com/richardschneider/ipfs-encryption 2017-12-06 22:56:09 +13:00
49e6c47c40 chore: setup repo 2017-12-06 08:49:16 +00:00
4c8d147c92 Initial commit 2017-12-06 08:24:22 +00:00
11 changed files with 1349 additions and 0 deletions

View File

@ -49,6 +49,7 @@
"debug": "^4.1.1",
"err-code": "^1.1.2",
"hashlru": "^2.3.0",
"interface-datastore": "^0.8.0",
"it-all": "^1.0.1",
"it-buffer": "^0.1.1",
"it-handshake": "^1.0.1",
@ -64,6 +65,7 @@
"multiaddr": "^7.2.1",
"multistream-select": "^0.15.0",
"mutable-proxy": "^1.0.0",
"node-forge": "^0.9.1",
"p-any": "^2.1.0",
"p-fifo": "^1.0.0",
"p-settle": "^3.1.0",
@ -71,6 +73,7 @@
"peer-info": "^0.17.0",
"protons": "^1.0.1",
"retimer": "^2.0.0",
"sanitize-filename": "^1.6.3",
"timeout-abort-controller": "^1.0.0",
"xsalsa20": "^1.0.2"
},
@ -80,12 +83,17 @@
"aegir": "^20.5.1",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-string": "^1.5.0",
"cids": "^0.7.1",
"datastore-fs": "^0.9.1",
"datastore-level": "^0.14.0",
"delay": "^4.3.0",
"dirty-chai": "^2.0.1",
"is-browser": "^2.1.0",
"it-concat": "^1.0.0",
"it-pair": "^1.0.0",
"it-pushable": "^1.4.0",
"level": "^6.0.0",
"libp2p-bootstrap": "^0.10.3",
"libp2p-delegated-content-routing": "^0.4.1",
"libp2p-delegated-peer-routing": "^0.4.0",
@ -98,10 +106,13 @@
"libp2p-tcp": "^0.14.1",
"libp2p-webrtc-star": "^0.17.0",
"libp2p-websockets": "^0.13.1",
"multihashes": "^0.4.15",
"nock": "^10.0.6",
"p-defer": "^3.0.0",
"p-times": "^2.1.0",
"p-wait-for": "^3.1.0",
"promisify-es6": "^1.0.3",
"rimraf": "^3.0.0",
"sinon": "^8.1.0",
"streaming-iterables": "^4.1.0",
"wrtc": "^0.4.1"

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

@ -0,0 +1,117 @@
# 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
### 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

View File

@ -0,0 +1,74 @@
/* 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 os = require('os')
const path = require('path')
const isBrowser = require('is-browser')
const FsStore = require('datastore-fs')
const LevelStore = require('datastore-level')
const Keychain = require('../../src/keychain')
describe('cms interop', () => {
const passPhrase = 'this is not a secure phrase'
const aliceKeyName = 'cms-interop-alice'
let ks
before(() => {
const datastore = isBrowser
? new LevelStore('test-keystore-1', { db: require('level') })
: new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now()))
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,394 @@
/* 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 os = require('os')
const path = require('path')
const isBrowser = require('is-browser')
const FsStore = require('datastore-fs')
const LevelStore = require('datastore-level')
const Keychain = require('../../src/keychain')
const PeerId = require('peer-id')
describe('keychain', () => {
const passPhrase = 'this is not a secure phrase'
const rsaKeyName = 'tajné jméno'
const renamedRsaKeyName = 'ชื่อลับ'
let rsaKeyInfo
let emptyKeystore
let ks
let datastore1, datastore2
before(() => {
datastore1 = isBrowser
? new LevelStore('test-keystore-1', { db: require('level') })
: new FsStore(path.join(os.tmpdir(), 'test-keystore-1-' + Date.now()))
datastore2 = isBrowser
? new LevelStore('test-keystore-2', { db: require('level') })
: new FsStore(path.join(os.tmpdir(), 'test-keystore-2-' + Date.now()))
ks = new Keychain(datastore2, { passPhrase: passPhrase })
emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase })
})
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)
})
})
})

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