Skip to content

Commit 5560669

Browse files
richardschneiderdaviddias
authored andcommitted
CMS - PKCS #7 (#19)
CMS - PKCS #7
1 parent acf48a8 commit 5560669

File tree

9 files changed

+384
-14
lines changed

9 files changed

+384
-14
lines changed

.travis.yml

-4
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@ matrix:
1414
script:
1515
- npm run lint
1616
- npm run test
17-
- npm run coverage
1817

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

23-
after_success:
24-
- npm run coverage-publish
25-
2622
addons:
2723
firefox: 'latest'
2824
apt:

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ A naming service for a key
6565

6666
Cryptographically protected messages
6767

68-
- `cms.createAnonymousEncryptedData (name, plain, callback)`
69-
- `cms.readData (cmsData, callback)`
68+
- `cms.encrypt (name, plain, callback)`
69+
- `cms.decrypt (cmsData, callback)`
7070

7171
### KeyInfo
7272

@@ -105,6 +105,10 @@ const defaultOptions = {
105105

106106
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.
107107

108+
### Cryptographic Message Syntax (CMS)
109+
110+
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.
111+
108112
## Contribute
109113

110114
Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)!

src/cms.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
'use strict'
2+
3+
const async = require('async')
4+
const forge = require('node-forge')
5+
const util = require('./util')
6+
7+
/**
8+
* Cryptographic Message Syntax (aka PKCS #7)
9+
*
10+
* CMS describes an encapsulation syntax for data protection. It
11+
* is used to digitally sign, digest, authenticate, or encrypt
12+
* arbitrary message content.
13+
*
14+
* See RFC 5652 for all the details.
15+
*/
16+
class CMS {
17+
/**
18+
* Creates a new instance with a keychain
19+
*
20+
* @param {Keychain} keychain - the available keys
21+
*/
22+
constructor (keychain) {
23+
if (!keychain) {
24+
throw new Error('keychain is required')
25+
}
26+
27+
this.keychain = keychain
28+
}
29+
30+
/**
31+
* Creates some protected data.
32+
*
33+
* The output Buffer contains the PKCS #7 message in DER.
34+
*
35+
* @param {string} name - The local key name.
36+
* @param {Buffer} plain - The data to encrypt.
37+
* @param {function(Error, Buffer)} callback
38+
* @returns {undefined}
39+
*/
40+
encrypt (name, plain, callback) {
41+
const self = this
42+
const done = (err, result) => async.setImmediate(() => callback(err, result))
43+
44+
if (!Buffer.isBuffer(plain)) {
45+
return done(new Error('Plain data must be a Buffer'))
46+
}
47+
48+
async.series([
49+
(cb) => self.keychain.findKeyByName(name, cb),
50+
(cb) => self.keychain._getPrivateKey(name, cb)
51+
], (err, results) => {
52+
if (err) return done(err)
53+
54+
let key = results[0]
55+
let pem = results[1]
56+
try {
57+
const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
58+
util.certificateForKey(key, privateKey, (err, certificate) => {
59+
if (err) return callback(err)
60+
61+
// create a p7 enveloped message
62+
const p7 = forge.pkcs7.createEnvelopedData()
63+
p7.addRecipient(certificate)
64+
p7.content = forge.util.createBuffer(plain)
65+
p7.encrypt()
66+
67+
// convert message to DER
68+
const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
69+
done(null, Buffer.from(der, 'binary'))
70+
})
71+
} catch (err) {
72+
done(err)
73+
}
74+
})
75+
}
76+
77+
/**
78+
* Reads some protected data.
79+
*
80+
* The keychain must contain one of the keys used to encrypt the data. If none of the keys
81+
* exists, an Error is returned with the property 'missingKeys'. It is array of key ids.
82+
*
83+
* @param {Buffer} cmsData - The CMS encrypted data to decrypt.
84+
* @param {function(Error, Buffer)} callback
85+
* @returns {undefined}
86+
*/
87+
decrypt (cmsData, callback) {
88+
const done = (err, result) => async.setImmediate(() => callback(err, result))
89+
90+
if (!Buffer.isBuffer(cmsData)) {
91+
return done(new Error('CMS data is required'))
92+
}
93+
94+
const self = this
95+
let cms
96+
try {
97+
const buf = forge.util.createBuffer(cmsData.toString('binary'))
98+
const obj = forge.asn1.fromDer(buf)
99+
cms = forge.pkcs7.messageFromAsn1(obj)
100+
} catch (err) {
101+
return done(new Error('Invalid CMS: ' + err.message))
102+
}
103+
104+
// Find a recipient whose key we hold. We only deal with recipient certs
105+
// issued by ipfs (O=ipfs).
106+
const recipients = cms.recipients
107+
.filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
108+
.filter(r => r.issuer.find(a => a.shortName === 'CN'))
109+
.map(r => {
110+
return {
111+
recipient: r,
112+
keyId: r.issuer.find(a => a.shortName === 'CN').value
113+
}
114+
})
115+
async.detect(
116+
recipients,
117+
(r, cb) => self.keychain.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
118+
(err, r) => {
119+
if (err) return done(err)
120+
if (!r) {
121+
const missingKeys = recipients.map(r => r.keyId)
122+
err = new Error('Decryption needs one of the key(s): ' + missingKeys.join(', '))
123+
err.missingKeys = missingKeys
124+
return done(err)
125+
}
126+
127+
async.waterfall([
128+
(cb) => self.keychain.findKeyById(r.keyId, cb),
129+
(key, cb) => self.keychain._getPrivateKey(key.name, cb)
130+
], (err, pem) => {
131+
if (err) return done(err)
132+
133+
const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
134+
cms.decrypt(r.recipient, privateKey)
135+
done(null, Buffer.from(cms.content.getBytes(), 'binary'))
136+
})
137+
}
138+
)
139+
}
140+
}
141+
142+
module.exports = CMS

src/keychain.js

+23-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const deepmerge = require('deepmerge')
66
const crypto = require('libp2p-crypto')
77
const DS = require('interface-datastore')
88
const pull = require('pull-stream')
9+
const CMS = require('./cms')
910

1011
const keyPrefix = '/pkcs8/'
1112
const infoPrefix = '/info/'
@@ -21,7 +22,7 @@ const defaultOptions = {
2122
// See https://cryptosense.com/parametesr-choice-for-pbkdf2/
2223
dek: {
2324
keyLength: 512 / 8,
24-
iterationCount: 1000,
25+
iterationCount: 10000,
2526
salt: 'you should override this value with a crypto secure random number',
2627
hash: 'sha2-512'
2728
}
@@ -86,8 +87,8 @@ function DsInfoName (name) {
8687
* Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
8788
*
8889
* A key in the store has two entries
89-
* - '/info/key-name', contains the KeyInfo for the key
90-
* - '/pkcs8/key-name', contains the PKCS #8 for the key
90+
* - '/info/*key-name*', contains the KeyInfo for the key
91+
* - '/pkcs8/*key-name*', contains the PKCS #8 for the key
9192
*
9293
*/
9394
class Keychain {
@@ -130,12 +131,17 @@ class Keychain {
130131
}
131132

132133
/**
133-
* The default options for a keychain.
134+
* Gets an object that can encrypt/decrypt protected data
135+
* using the Cryptographic Message Syntax (CMS).
134136
*
135-
* @returns {object}
137+
* CMS describes an encapsulation syntax for data protection. It
138+
* is used to digitally sign, digest, authenticate, or encrypt
139+
* arbitrary message content.
140+
*
141+
* @returns {CMS}
136142
*/
137-
static get options () {
138-
return defaultOptions
143+
get cms () {
144+
return new CMS(this)
139145
}
140146

141147
/**
@@ -150,6 +156,16 @@ class Keychain {
150156
return options
151157
}
152158

159+
/**
160+
* Gets an object that can encrypt/decrypt protected data.
161+
* The default options for a keychain.
162+
*
163+
* @returns {object}
164+
*/
165+
static get options () {
166+
return defaultOptions
167+
}
168+
153169
/**
154170
* Create a new key.
155171
*

src/util.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict'
2+
3+
const forge = require('node-forge')
4+
const pki = forge.pki
5+
exports = module.exports
6+
7+
/**
8+
* Gets a self-signed X.509 certificate for the key.
9+
*
10+
* The output Buffer contains the PKCS #7 message in DER.
11+
*
12+
* TODO: move to libp2p-crypto package
13+
*
14+
* @param {KeyInfo} key - The id and name of the key
15+
* @param {RsaPrivateKey} privateKey - The naked key
16+
* @param {function(Error, Certificate)} callback
17+
* @returns {undefined}
18+
*/
19+
exports.certificateForKey = (key, privateKey, callback) => {
20+
const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
21+
const cert = pki.createCertificate()
22+
cert.publicKey = publicKey
23+
cert.serialNumber = '01'
24+
cert.validity.notBefore = new Date()
25+
cert.validity.notAfter = new Date()
26+
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
27+
const attrs = [{
28+
name: 'organizationName',
29+
value: 'ipfs'
30+
}, {
31+
shortName: 'OU',
32+
value: 'keystore'
33+
}, {
34+
name: 'commonName',
35+
value: key.id
36+
}]
37+
cert.setSubject(attrs)
38+
cert.setIssuer(attrs)
39+
cert.setExtensions([{
40+
name: 'basicConstraints',
41+
cA: true
42+
}, {
43+
name: 'keyUsage',
44+
keyCertSign: true,
45+
digitalSignature: true,
46+
nonRepudiation: true,
47+
keyEncipherment: true,
48+
dataEncipherment: true
49+
}, {
50+
name: 'extKeyUsage',
51+
serverAuth: true,
52+
clientAuth: true,
53+
codeSigning: true,
54+
emailProtection: true,
55+
timeStamping: true
56+
}, {
57+
name: 'nsCertType',
58+
client: true,
59+
server: true,
60+
email: true,
61+
objsign: true,
62+
sslCA: true,
63+
emailCA: true,
64+
objCA: true
65+
}])
66+
// self-sign certificate
67+
cert.sign(privateKey)
68+
69+
return callback(null, cert)
70+
}

test/browser.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ describe('browser', () => {
2323
})
2424

2525
require('./keychain.spec')(datastore1, datastore2)
26+
require('./cms-interop')(datastore2)
2627
require('./peerid')
2728
})

test/cms-interop.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint max-nested-callbacks: ["error", 8] */
2+
/* eslint-env mocha */
3+
'use strict'
4+
5+
const chai = require('chai')
6+
const dirtyChai = require('dirty-chai')
7+
const expect = chai.expect
8+
chai.use(dirtyChai)
9+
chai.use(require('chai-string'))
10+
const Keychain = require('..')
11+
12+
module.exports = (datastore) => {
13+
describe('cms interop', () => {
14+
const passPhrase = 'this is not a secure phrase'
15+
const aliceKeyName = 'cms-interop-alice'
16+
let ks
17+
18+
before((done) => {
19+
ks = new Keychain(datastore, { passPhrase: passPhrase })
20+
done()
21+
})
22+
23+
const plainData = Buffer.from('This is a message from Alice to Bob')
24+
25+
it('imports openssl key', function (done) {
26+
this.timeout(10 * 1000)
27+
const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA'
28+
const alice = `-----BEGIN ENCRYPTED PRIVATE KEY-----
29+
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA
30+
MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG
31+
QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd
32+
1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7
33+
/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A
34+
CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri
35+
dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA
36+
ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY
37+
zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/
38+
ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt
39+
0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83
40+
GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH
41+
igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m
42+
3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE
43+
cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL
44+
-----END ENCRYPTED PRIVATE KEY-----
45+
`
46+
ks.importKey(aliceKeyName, alice, 'mypassword', (err, key) => {
47+
expect(err).to.not.exist()
48+
expect(key.name).to.equal(aliceKeyName)
49+
expect(key.id).to.equal(aliceKid)
50+
done()
51+
})
52+
})
53+
54+
it('decrypts node-forge example', (done) => {
55+
const example = `
56+
MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK
57+
EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI
58+
WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B
59+
AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k
60+
d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO
61+
knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3
62+
DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B
63+
nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N
64+
`
65+
ks.cms.decrypt(Buffer.from(example, 'base64'), (err, plain) => {
66+
expect(err).to.not.exist()
67+
expect(plain).to.exist()
68+
expect(plain.toString()).to.equal(plainData.toString())
69+
done()
70+
})
71+
})
72+
})
73+
}

0 commit comments

Comments
 (0)