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

CMS - PKCS #7 #19

Merged
merged 11 commits into from
Jan 29, 2018
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ matrix:
script:
- npm run lint
- npm run test
- npm run coverage

before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start

after_success:
- npm run coverage-publish

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem that coverage got added to Circle https://github.com/libp2p/js-libp2p-keychain/blob/master/circle.yml

addons:
firefox: 'latest'
apt:
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ A naming service for a key

Cryptographically protected messages

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

### KeyInfo

Expand Down Expand Up @@ -105,6 +105,10 @@ const defaultOptions = {

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-crypto/issues)!
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"homepage": "https://github.com/libp2p/js-libp2p-keychain#readme",
"dependencies": {
"async": "^2.6.0",
"deepmerge": "^2.0.1",
"deepmerge": "^1.5.2",
"interface-datastore": "~0.4.2",
"libp2p-crypto": "~0.12.0",
"pull-stream": "^3.6.1",
Expand Down
142 changes: 142 additions & 0 deletions src/cms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use strict'

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

/**
* 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 new Error('keychain is 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.
* @param {function(Error, Buffer)} callback
* @returns {undefined}
*/
encrypt (name, plain, callback) {
const self = this
const done = (err, result) => async.setImmediate(() => callback(err, result))

if (!Buffer.isBuffer(plain)) {
return done(new Error('Plain data must be a Buffer'))
}

async.series([
(cb) => self.keychain.findKeyByName(name, cb),
(cb) => self.keychain._getPrivateKey(name, cb)
], (err, results) => {
if (err) return done(err)

let key = results[0]
let pem = results[1]
try {
const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
util.certificateForKey(key, 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()
done(null, Buffer.from(der, 'binary'))
})
} catch (err) {
done(err)
}
})
}

/**
* 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.
* @param {function(Error, Buffer)} callback
* @returns {undefined}
*/
decrypt (cmsData, callback) {
const done = (err, result) => async.setImmediate(() => callback(err, result))

if (!Buffer.isBuffer(cmsData)) {
return done(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 done(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.keychain.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
(err, r) => {
if (err) return done(err)
if (!r) {
const missingKeys = recipients.map(r => r.keyId)
err = new Error('Decryption needs one of the key(s): ' + missingKeys.join(', '))
err.missingKeys = missingKeys
return done(err)
}

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

const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
cms.decrypt(r.recipient, privateKey)
done(null, Buffer.from(cms.content.getBytes(), 'binary'))
})
}
)
}
}

module.exports = CMS
30 changes: 23 additions & 7 deletions src/keychain.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const deepmerge = require('deepmerge')
const crypto = require('libp2p-crypto')
const DS = require('interface-datastore')
const pull = require('pull-stream')
const CMS = require('./cms')

const keyPrefix = '/pkcs8/'
const infoPrefix = '/info/'
Expand All @@ -21,7 +22,7 @@ const defaultOptions = {
// See https://cryptosense.com/parametesr-choice-for-pbkdf2/
dek: {
keyLength: 512 / 8,
iterationCount: 1000,
iterationCount: 10000,
salt: 'you should override this value with a crypto secure random number',
hash: 'sha2-512'
}
Expand Down Expand Up @@ -86,8 +87,8 @@ function DsInfoName (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
* - '/info/*key-name*', contains the KeyInfo for the key
* - '/pkcs8/*key-name*', contains the PKCS #8 for the key
*
*/
class Keychain {
Expand Down Expand Up @@ -130,12 +131,17 @@ class Keychain {
}

/**
* The default options for a keychain.
* Gets an object that can encrypt/decrypt protected data
* using the Cryptographic Message Syntax (CMS).
*
* @returns {object}
* CMS describes an encapsulation syntax for data protection. It
* is used to digitally sign, digest, authenticate, or encrypt
* arbitrary message content.
*
* @returns {CMS}
*/
static get options () {
return defaultOptions
get cms () {
return new CMS(this)
}

/**
Expand All @@ -150,6 +156,16 @@ class Keychain {
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.
*
Expand Down
70 changes: 70 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict'

const forge = require('node-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
* @param {function(Error, Certificate)} callback
* @returns {undefined}
*/
exports.certificateForKey = (key, privateKey, callback) => {
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 callback(null, cert)
}
1 change: 1 addition & 0 deletions test/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ describe('browser', () => {
})

require('./keychain.spec')(datastore1, datastore2)
require('./cms-interop')(datastore2)
require('./peerid')
})
Loading