This repository has been archived by the owner on Jun 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
384 additions
and
14 deletions.
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
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,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 |
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,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) | ||
} |
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,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() | ||
}) | ||
}) | ||
}) | ||
} |
Oops, something went wrong.