-
Notifications
You must be signed in to change notification settings - Fork 453
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: integrate libp2p-keychain into js-libp2p (#633)
Integrates the libp2p-keychain codebase into this repo Co-authored-by: David Dias <[email protected]> Co-authored-by: Richard Schneider <[email protected]> Co-authored-by: Maciej Krüger <[email protected]> Co-authored-by: Victor Bjelkholm <[email protected]> Co-authored-by: Masahiro Saito <[email protected]> Co-authored-by: Alan Shaw <[email protected]> Co-authored-by: Hugo Dias <[email protected]> Co-authored-by: Alberto Elias <[email protected]> Co-authored-by: Alex Potsides <[email protected]> Co-authored-by: Jacob Heun <[email protected]>
- Loading branch information
1 parent
6627278
commit 8bf5a70
Showing
13 changed files
with
1,393 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
'use strict' | ||
|
||
module.exports = require('./keychain') |
Oops, something went wrong.