Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
richardschneider committed Dec 6, 2017
1 parent 49e6c47 commit 1a96ae8
Show file tree
Hide file tree
Showing 14 changed files with 1,178 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.png binary
* crlf=input
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,91 @@
![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D6.0.0-orange.svg?style=flat-square)

> Keychain primitives for libp2p in JavaScript
> A secure key chain for libp2p in JavaScript
## Features

- Manages the lifecycle of a key
- Keys are encrypted at rest
- Enforces the use of safe key names
- Uses encrypted PKCS 8 for key storage
- Uses PBKDF2 for a "stetched" key encryption key
- Enforces NIST SP 800-131A and NIST SP 800-132
- Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages
- Delays reporting errors to slow down brute force attacks

## Table of Contents

## Install

### Usage

const datastore = new FsStore('./a-keystore')
const opts = {
passPhrase: 'some long easily remembered phrase'
}
const keychain = new Keychain(datastore, opts)

## API

Managing a key

- `createKey (name, type, size, callback)`
- `renameKey (oldName, newName, callback)`
- `removeKey (name, callback)`
- `exportKey (name, password, callback)`
- `importKey (name, pem, password, callback)`
- `importPeer (name, peer, callback)`

A naming service for a key

- `listKeys (callback)`
- `findKeyById (id, callback)`
- `findKeyByName (name, callback)`

Cryptographically protected messages

- `cms.createAnonymousEncryptedData (name, plain, callback)`
- `cms.readData (cmsData, callback)`

### KeyInfo

The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain.

```
{
name: 'rsa-key',
id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW'
}
```

The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt).

### Private key storage

A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. Its file extension is `.p8`.

The default options for generating the derived encryption key are in the `dek` object
```
const defaultOptions = {
createIfNeeded: true,
//See https://cryptosense.com/parameter-choice-for-pbkdf2/
dek: {
keyLength: 512 / 8,
iterationCount: 10000,
salt: 'you should override this value with a crypto secure random number',
hash: 'sha512'
}
}
```

![key storage](../doc/private-key.png?raw=true)

### Physical storage

The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benifit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation.

## Contribute

Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)!
Expand Down
Binary file added doc/private-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/private-key.xml
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>
29 changes: 25 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "libp2p-keychain",
"version": "0.0.0",
"description": "",
"version": "0.1.0",
"description": "Key management and cryptographically protected messages",
"main": "src/index.js",
"scripts": {
"lint": "aegir lint",
Expand Down Expand Up @@ -29,16 +29,37 @@
"IPFS",
"libp2p",
"keys",
"encryption",
"secure",
"crypto"
],
"author": "David Dias <daviddias@ipfs.io>",
"author": "Richard Schneider <makaretu@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/libp2p/js-libp2p-keychain/issues"
},
"homepage": "https://github.com/libp2p/js-libp2p-keychain#readme",
"dependencies": {
"async": "^2.6.0",
"deepmerge": "^1.5.2",
"interface-datastore": "~0.4.1",
"libp2p-crypto": "~0.10.3",
"multihashes": "~0.4.12",
"node-forge": "~0.7.1",
"pull-stream": "^3.6.1",
"sanitize-filename": "^1.6.1"
},
"devDependencies": {
"aegir": "^12.2.0",
"pre-commit": "^1.2.2"
"chai": "^4.1.2",
"chai-string": "^1.4.0",
"datastore-fs": "^0.4.1",
"datastore-level": "^0.7.0",
"dirty-chai": "^2.0.1",
"level-js": "^2.2.4",
"mocha": "^4.0.1",
"peer-id": "^0.10.2",
"pre-commit": "^1.2.2",
"rimraf": "^2.6.2"
}
}
97 changes: 97 additions & 0 deletions src/cms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict'

const async = require('async')
const forge = require('node-forge')
const util = require('./util')

class CMS {
constructor (keystore) {
if (!keystore) {
throw new Error('keystore is required')
}

this.keystore = keystore;
}

createAnonymousEncryptedData (name, plain, callback) {
const self = this
if (!Buffer.isBuffer(plain)) {
return callback(new Error('Data is required'))
}

self.keystore._getPrivateKey(name, (err, key) => {
if (err) {
return callback(err)
}

try {
const privateKey = forge.pki.decryptRsaPrivateKey(key, self.keystore._())
util.certificateForKey(privateKey, (err, certificate) => {
if (err) return callback(err)

// create a p7 enveloped message
const p7 = forge.pkcs7.createEnvelopedData()
p7.addRecipient(certificate)
p7.content = forge.util.createBuffer(plain)
p7.encrypt()

// convert message to DER
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
callback(null, Buffer.from(der, 'binary'))
})
} catch (err) {
callback(err)
}
})
}

readData (cmsData, callback) {
if (!Buffer.isBuffer(cmsData)) {
return callback(new Error('CMS data is required'))
}

const self = this
let cms
try {
const buf = forge.util.createBuffer(cmsData.toString('binary'));
const obj = forge.asn1.fromDer(buf)
cms = forge.pkcs7.messageFromAsn1(obj)
} catch (err) {
return callback(new Error('Invalid CMS: ' + err.message))
}

// Find a recipient whose key we hold. We only deal with recipient certs
// issued by ipfs (O=ipfs).
const recipients = cms.recipients
.filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
.filter(r => r.issuer.find(a => a.shortName === 'CN'))
.map(r => {
return {
recipient: r,
keyId: r.issuer.find(a => a.shortName === 'CN').value
}
})
async.detect(
recipients,
(r, cb) => self.keystore.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
(err, r) => {
if (err) return callback(err)
if (!r) return callback(new Error('No key found for decryption'))

async.waterfall([
(cb) => self.keystore.findKeyById(r.keyId, cb),
(key, cb) => self.keystore._getPrivateKey(key.name, cb)
], (err, pem) => {
if (err) return callback(err);

const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keystore._())
cms.decrypt(r.recipient, privateKey)
async.setImmediate(() => callback(null, Buffer.from(cms.content.getBytes(), 'binary')))
})
}
)
}

}

module.exports = CMS
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
'use strict'

module.exports = require('./keychain')
Loading

0 comments on commit 1a96ae8

Please sign in to comment.