mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-06-03 04:31:19 +00:00
parent
acf48a8efe
commit
5560669fc9
@ -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
|
||||
|
||||
addons:
|
||||
firefox: 'latest'
|
||||
apt:
|
||||
|
@ -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
|
||||
|
||||
@ -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)!
|
||||
|
142
src/cms.js
Normal file
142
src/cms.js
Normal file
@ -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
|
@ -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/'
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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.
|
||||
*
|
||||
|
70
src/util.js
Normal file
70
src/util.js
Normal file
@ -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)
|
||||
}
|
@ -23,5 +23,6 @@ describe('browser', () => {
|
||||
})
|
||||
|
||||
require('./keychain.spec')(datastore1, datastore2)
|
||||
require('./cms-interop')(datastore2)
|
||||
require('./peerid')
|
||||
})
|
||||
|
73
test/cms-interop.js
Normal file
73
test/cms-interop.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* 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 Keychain = require('..')
|
||||
|
||||
module.exports = (datastore) => {
|
||||
describe('cms interop', () => {
|
||||
const passPhrase = 'this is not a secure phrase'
|
||||
const aliceKeyName = 'cms-interop-alice'
|
||||
let ks
|
||||
|
||||
before((done) => {
|
||||
ks = new Keychain(datastore, { passPhrase: passPhrase })
|
||||
done()
|
||||
})
|
||||
|
||||
const plainData = Buffer.from('This is a message from Alice to Bob')
|
||||
|
||||
it('imports openssl key', function (done) {
|
||||
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-----
|
||||
`
|
||||
ks.importKey(aliceKeyName, alice, 'mypassword', (err, key) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(key.name).to.equal(aliceKeyName)
|
||||
expect(key.id).to.equal(aliceKid)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('decrypts node-forge example', (done) => {
|
||||
const example = `
|
||||
MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK
|
||||
EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI
|
||||
WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B
|
||||
AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k
|
||||
d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO
|
||||
knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3
|
||||
DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B
|
||||
nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N
|
||||
`
|
||||
ks.cms.decrypt(Buffer.from(example, 'base64'), (err, plain) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(plain).to.exist()
|
||||
expect(plain.toString()).to.equal(plainData.toString())
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -16,11 +16,12 @@ module.exports = (datastore1, datastore2) => {
|
||||
const rsaKeyName = 'tajné jméno'
|
||||
const renamedRsaKeyName = 'ชื่อลับ'
|
||||
let rsaKeyInfo
|
||||
// let emptyKeystore
|
||||
let emptyKeystore
|
||||
let ks
|
||||
|
||||
before((done) => {
|
||||
ks = new Keychain(datastore2, { passPhrase: passPhrase })
|
||||
emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase })
|
||||
done()
|
||||
})
|
||||
|
||||
@ -169,6 +170,72 @@ module.exports = (datastore1, datastore2) => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('CMS protected data', () => {
|
||||
const plainData = Buffer.from('This is a message from Alice to Bob')
|
||||
let cms
|
||||
|
||||
it('service is available', (done) => {
|
||||
expect(ks).to.have.property('cms')
|
||||
done()
|
||||
})
|
||||
|
||||
it('requires a key', (done) => {
|
||||
ks.cms.encrypt('no-key', plainData, (err, msg) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('requires plain data as a Buffer', (done) => {
|
||||
ks.cms.encrypt(rsaKeyName, 'plain data', (err, msg) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('encrypts', (done) => {
|
||||
ks.cms.encrypt(rsaKeyName, plainData, (err, msg) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(msg).to.exist()
|
||||
expect(msg).to.be.instanceOf(Buffer)
|
||||
cms = msg
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('is a PKCS #7 message', (done) => {
|
||||
ks.cms.decrypt('not CMS', (err) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('is a PKCS #7 binary message', (done) => {
|
||||
ks.cms.decrypt(plainData, (err) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot be read without the key', (done) => {
|
||||
emptyKeystore.cms.decrypt(cms, (err, plain) => {
|
||||
expect(err).to.exist()
|
||||
expect(err).to.have.property('missingKeys')
|
||||
expect(err.missingKeys).to.eql([rsaKeyInfo.id])
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('can be read with the key', (done) => {
|
||||
ks.cms.decrypt(cms, (err, plain) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(plain).to.exist()
|
||||
expect(plain.toString()).to.equal(plainData.toString())
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('exported key', () => {
|
||||
let pemKey
|
||||
|
||||
|
@ -30,5 +30,6 @@ describe('node', () => {
|
||||
})
|
||||
|
||||
require('./keychain.spec')(datastore1, datastore2)
|
||||
require('./cms-interop')(datastore2)
|
||||
require('./peerid')
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user