From fe56829ddaea857b537dd2f0df960837f13a17b6 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 14 Dec 2017 19:43:30 +1300 Subject: [PATCH 01/11] feat: export/import password protected RSA key --- package.json | 2 ++ src/keys/index.js | 16 +++++++++++++ src/keys/rsa-browser.js | 4 +--- src/keys/rsa-class.js | 50 ++++++++++++++++++++++++++++++++++++++++- test/keys/rsa.spec.js | 27 ++++++++++++++++++++++ 5 files changed, 95 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ae1c5892..f204e417 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "asn1.js": "^5.0.0", "async": "^2.6.0", "browserify-aes": "^1.1.1", + "jsrsasign": "^8.0.4", "keypair": "^1.0.1", "libp2p-crypto-secp256k1": "~0.2.2", "multihashing-async": "~0.4.7", @@ -46,6 +47,7 @@ "aegir": "^12.2.0", "benchmark": "^2.1.4", "chai": "^4.1.2", + "chai-string": "^1.4.0", "dirty-chai": "^2.0.1", "pre-commit": "^1.2.2" }, diff --git a/src/keys/index.js b/src/keys/index.js index a8ce873d..7712558b 100644 --- a/src/keys/index.js +++ b/src/keys/index.js @@ -2,6 +2,8 @@ const protobuf = require('protons') const keysPBM = protobuf(require('./keys.proto')) +const jsrsasign = require('jsrsasign') +const KEYUTIL = jsrsasign.KEYUTIL exports = module.exports @@ -115,3 +117,17 @@ exports.marshalPrivateKey = (key, type) => { return key.bytes } + +exports.import = (pem, password, callback) => { + try { + const key = KEYUTIL.getKey(pem, password) + if (key instanceof jsrsasign.RSAKey) { + const jwk = KEYUTIL.getJWKFromKey(key) + return supportedKeys.rsa.fromJwk(jwk, callback) + } else { + throw new Error(`Unknown key type '${key.prototype.toString()}'`) + } + } catch (err) { + callback(err) + } +} diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-browser.js index 8568b927..d23bd0ba 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -105,9 +105,7 @@ function derivePublicFromPrivate (jwKey) { { kty: jwKey.kty, n: jwKey.n, - e: jwKey.e, - alg: jwKey.alg, - kid: jwKey.kid + e: jwKey.e }, { name: 'RSASSA-PKCS1-v1_5', diff --git a/src/keys/rsa-class.js b/src/keys/rsa-class.js index 353527d1..f7ea2c00 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -5,6 +5,8 @@ const protobuf = require('protons') const crypto = require('./rsa') const pbm = protobuf(require('./keys.proto')) +const KEYUTIL = require('jsrsasign').KEYUTIL +const setImmediate = require('async/setImmediate') class RsaPublicKey { constructor (key) { @@ -89,6 +91,41 @@ class RsaPrivateKey { ensure(callback) multihashing(this.bytes, 'sha2-256', callback) } + + /** + * Exports the key into a password protected PEM format + * + * @param {string} [format] - Defaults to 'pkcs-8'. + * @param {string} password - The password to read the encrypted PEM + * @param {function(Error, KeyInfo)} callback + * @returns {undefined} + */ + export (format, password, callback) { + if (typeof password === 'function') { + callback = password + password = format + format = 'pkcs-8' + } + + setImmediate(() => { + ensure(callback) + + let err = null + let pem = null + try { + const key = KEYUTIL.getKey(this._key) // _key is a JWK (JSON Web Key) + if (format === 'pkcs-8') { + pem = KEYUTIL.getPEM(key, 'PKCS8PRV', password) + } else { + err = new Error(`Unknown export format '${format}'`) + } + } catch (e) { + err = e + } + + callback(err, pem) + }) + } } function unmarshalRsaPrivateKey (bytes, callback) { @@ -108,6 +145,16 @@ function unmarshalRsaPublicKey (bytes) { return new RsaPublicKey(jwk) } +function fromJwk (jwk, callback) { + crypto.unmarshalPrivateKey(jwk, (err, keys) => { + if (err) { + return callback(err) + } + + callback(null, new RsaPrivateKey(keys.privateKey, keys.publicKey)) + }) +} + function generateKeyPair (bits, cb) { crypto.generateKey(bits, (err, keys) => { if (err) { @@ -129,5 +176,6 @@ module.exports = { RsaPrivateKey, unmarshalRsaPublicKey, unmarshalRsaPrivateKey, - generateKeyPair + generateKeyPair, + fromJwk } diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index 781bf19d..e98f0eab 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -5,6 +5,7 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +chai.use(require('chai-string')) const crypto = require('../../src') const rsa = crypto.keys.supportedKeys.rsa @@ -134,6 +135,32 @@ describe('RSA', function () { }) }) + it('exports and imports encrypted PKCS #8', (done) => { + key.export('pkcs-8', 'my secret', (err, pem) => { + expect(err).to.not.exist() + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + crypto.keys.import(pem, 'my secret', (err, clone) => { + expect(err).to.not.exist() + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + done() + }) + }) + }) + + it('exports defaults to encrypted PKCS #8', (done) => { + key.export('another secret', (err, pem) => { + expect(err).to.not.exist() + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + crypto.keys.import(pem, 'another secret', (err, clone) => { + expect(err).to.not.exist() + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + done() + }) + }) + }) + describe('returns error via cb instead of crashing', () => { const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) testGarbage.doTests('key.verify', key.verify.bind(key), 2, true) From b8c1a2f333e118023776a91f92c51fefe9d7cbf2 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 14 Dec 2017 20:23:10 +1300 Subject: [PATCH 02/11] docs: crypto.key.import --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 02ab8621..2de7487f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ This repo contains the JavaScript implementation of the crypto primitives needed - [`unmarshalPublicKey(buf)`](#unmarshalpublickeybuf) - [`marshalPrivateKey(key[, type])`](#marshalprivatekeykey-type) - [`unmarshalPrivateKey(buf, callback)`](#unmarshalprivatekeybuf-callback) + - [`import(pem, password, callback)`](#importpem-password-callback) - [`webcrypto`](#webcrypto) - [Contribute](#contribute) - [License](#license) @@ -183,6 +184,14 @@ Converts a private key object into a protobuf serialized private key. Converts a protobuf serialized private key into its representative object. +### `crypto.keys.import(pem, password, callback)` + +- `pem: string` +- `password: string` +- `callback: Function` + +Converts a PEM password protected private key into its representative object. + ### `crypto.randomBytes(number)` - `number: Number` From 2b111d12609abc890c5066d103ae42969b6c0cb8 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 14 Dec 2017 20:34:37 +1300 Subject: [PATCH 03/11] test: import with wrong password --- test/keys/rsa.spec.js | 46 +++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index e98f0eab..6ff3ca23 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -135,28 +135,40 @@ describe('RSA', function () { }) }) - it('exports and imports encrypted PKCS #8', (done) => { - key.export('pkcs-8', 'my secret', (err, pem) => { - expect(err).to.not.exist() - expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - crypto.keys.import(pem, 'my secret', (err, clone) => { + describe('export and import', () => { + it('password protected PKCS #8', (done) => { + key.export('pkcs-8', 'my secret', (err, pem) => { expect(err).to.not.exist() - expect(clone).to.exist() - expect(key.equals(clone)).to.eql(true) - done() + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + crypto.keys.import(pem, 'my secret', (err, clone) => { + expect(err).to.not.exist() + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + done() + }) }) }) - }) - it('exports defaults to encrypted PKCS #8', (done) => { - key.export('another secret', (err, pem) => { - expect(err).to.not.exist() - expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') - crypto.keys.import(pem, 'another secret', (err, clone) => { + it('defaults to PKCS #8', (done) => { + key.export('another secret', (err, pem) => { expect(err).to.not.exist() - expect(clone).to.exist() - expect(key.equals(clone)).to.eql(true) - done() + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + crypto.keys.import(pem, 'another secret', (err, clone) => { + expect(err).to.not.exist() + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + done() + }) + }) + }) + + it('needs correct password', (done) => { + key.export('another secret', (err, pem) => { + expect(err).to.not.exist() + crypto.keys.import(pem, 'not the secret', (err, clone) => { + expect(err).to.exist() + done() + }) }) }) }) From 5c77d7c8f189ab23d17654b7d8e539d45fcd6464 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Thu, 14 Dec 2017 21:02:33 +1300 Subject: [PATCH 04/11] fix: lint --- test/keys/rsa.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index 6ff3ca23..4803f98c 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -1,3 +1,4 @@ +/* eslint max-nested-callbacks: ["error", 8] */ /* eslint-env mocha */ 'use strict' From 57952069b6631ff896383f5beb4da3bfa09e691f Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Mon, 18 Dec 2017 21:07:59 +1300 Subject: [PATCH 05/11] test: importing openssl keys --- test/keys/rsa.spec.js | 162 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index 4803f98c..917ee9b6 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -192,4 +192,166 @@ describe('RSA', function () { }) }) }) + + describe('openssl interop', () => { + it('can read a private key', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:3072 + * -pkeyopt rsa_keygen_pubexp:65537 + */ + const pem = `-----BEGIN PRIVATE KEY----- +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 +4pMwXeXP+LO8NIfRXV8mgrm86g== +-----END PRIVATE KEY----- +` + crypto.keys.import(pem, '', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + done() + }) + }) + + // AssertionError: expected 'this only supports pkcs5PBES2' to not exist + it.skip('can read a private encrypted key (v1)', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQI2563Jugj/KkCAggABIICgPxHkKtUUE8EWevq +eX9nTjqpbsv0QoXQMhegfxDELJLU8tj6V0bWNt7QDdfQ1n6FRgnNvNGick6gyqHH +yH9qC2oXwkDFP7OrHp2NEZd7DHQLLc+L4KJ/0dzsiZ1U9no7XzQMUay9Bc918ADE +pN2/EqigWkaG4gNjkAeKWr6+BNRevDXlSvls7YDboNcTiACi5zJkthivB9g3vT1m +gPdN6Gf/mmqtBTDHeqj5QsmXYqeCyo5b26JgYsziABVZDHph4ekPUsTvudRpE9Ex +baXwdYEAZxVpSbTvQ3A5qysjSZeM9ttfRTSSwL391q7dViz4+aujpk0Vj7piH+1B +CkfO8/XudRdRlnOe+KjMidktKCsMGCIOW92IlfMvIQ/Zn1GTYj9bRXONFNJ2WPND +UmCKnL7cmworwg/weRorrGKBWIGspU+tDASOPSvIGKo6Hoxm4CN1TpDRY7DAGlgm +Y3TEbMYfpXyzkPjvAhJDt03D3J9PrTO6uM5d7YUaaTmJ2TQFQVF2Lc3Uz8lDJLs0 +ZYtfQ/4H+YY2RrX7ua7t6ArUcYXZtv0J4lRYWjwV8fGPUVc0d8xLJU0Yjf4BD7K8 +rsavHo9b5YvBUX7SgUyxAEembEOe3SjQ+gPu2U5wovcjUuC9eItEEsXGrx30BQ0E +8BtK2+hp0eMkW5/BYckJkH+Yl8ypbzRGRRIZzLgeI4JveSx/mNhewfgTr+ORPThZ +mBdkD5r+ixWF174naw53L8U9wF8kiK7pIE1N9TR4USEeovLwX6Ni/2MMDZedOfof +2f77eUdLsK19/5/lcgAAYaXauXWhy2d2r3SayFrC9woy0lh2VLKRMBjcx1oWb7dp +0uxzo5Y= +-----END ENCRYPTED PRIVATE KEY----- +` + crypto.keys.import(pem, 'mypassword', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + done() + }) + }) + + // AssertionError: expected 'this only supports TripleDES' to not exist + it.skip('can read a private encrypted key (v2 aes-256-cbc)', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-256-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIhuL894loRucCAggA +MB0GCWCGSAFlAwQBKgQQEoEtsjW3iC9/u0uGvkxX7wSCAoAsX3l6JoR2OGbT8CkY +YT3RQFqquOgItYOHw6E3tir2YrmxEAo99nxoL8pdto37KSC32eAGnfv5R1zmHHSx +0M3/y2AWiCBTX95EEzdtGC1hK3PBa/qpp/xEmcrsjYN6NXxMAkhC0hMP/HdvqMAg +ee7upvaYJsJcl8QLFNayAWr8b8cZA/RBhGEIRl59Eyj6nNtxDt3bCrfe06o1CPCV +50/fRZEwFOi/C6GYvPN6MrPZO3ALBWgopLT2yQqycTKtfxYWIdOsMBkAjKf2D6Pk +u2mqBsaP4b71jIIeT4euSJLsoJV+O39s8YHXtW8GtOqp7V5kIlnm90lZ9wzeLTZ7 +HJsD/jEdYto5J3YWm2wwEDccraffJSm7UDtJBvQdIx832kxeFCcGQjW38Zl1qqkg +iTH1PLTypxj2ZuviS2EkXVFb/kVU6leWwOt6fqWFC58UvJKeCk/6veazz3PDnTWM +92ClUqFd+CZn9VT4CIaJaAc6v5NLpPp+T9sRX9AtequPm7FyTeevY9bElfyk9gW9 +JDKgKxs6DGWDa16RL5vzwtU+G3o6w6IU+mEwa6/c+hN+pRFs/KBNLLSP9OHBx7BJ +X/32Ft+VFhJaK+lQ+f+hve7od/bgKnz4c/Vtp7Dh51DgWgCpBgb8p0vqu02vTnxD +BXtDv3h75l5PhvdWfVIzpMWRYFvPR+vJi066FjAz2sjYc0NMLSYtZWyWoIInjhoX +Dp5CQujCtw/ZSSlwde1DKEWAW4SeDZAOQNvuz0rU3eosNUJxEmh3aSrcrRtDpw+Y +mBUuWAZMpz7njBi7h+JDfmSW/GAaMwrVFC2gef5375R0TejAh+COAjItyoeYEvv8 +DQd8 +-----END ENCRYPTED PRIVATE KEY----- +` + crypto.keys.import(pem, 'mypassword', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + done() + }) + }) + + it('can read a private encrypted key (v2 des3)', (done) => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 des3 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISznrfHd+D58CAggA +MBQGCCqGSIb3DQMHBAhx0DnnUvDiHASCAoCceplm+Cmwlgvn4hNsv6e4c/S1iA7w +2hU7Jt8JgRCIMWjP2FthXOAFLa2fD4g3qncYXcDAFBXNyoh25OgOwstO14YkxhDi +wG4TeppGUt9IlyyCol6Z4WhQs1TGm5OcD5xDta+zBXsBnlgmKLD5ZXPEYB+3v/Dg +SvM4sQz6NgkVHN52hchERsnknwSOghiK9mIBH0RZU5LgzlDy2VoBCiEPVdZ7m4F2 +dft5e82zFS58vwDeNN/0r7fC54TyJf/8k3q94+4Hp0mseZ67LR39cvnEKuDuFROm +kLPLekWt5R2NGdunSQlA79BkrNB1ADruO8hQOOHMO9Y3/gNPWLKk+qrfHcUni+w3 +Ofq+rdfakHRb8D6PUmsp3wQj6fSOwOyq3S50VwP4P02gKcZ1om1RvEzTbVMyL3sh +hZcVB3vViu3DO2/56wo29lPVTpj9bSYjw/CO5jNpPBab0B/Gv7JAR0z4Q8gn6OPy +qf+ddyW4Kcb6QUtMrYepghDthOiS3YJV/zCNdL3gTtVs5Ku9QwQ8FeM0/5oJZPlC +TxGuOFEJnYRWqIdByCP8mp/qXS5alSR4uoYQSd7vZG4vkhkPNSAwux/qK1IWfqiW +3XlZzrbD//9IzFVqGRs4nRIFq85ULK0zAR57HEKIwGyn2brEJzrxpV6xsHBp+m4w +6r0+PtwuWA0NauTCUzJ1biUdH8t0TgBL6YLaMjlrfU7JstH3TpcZzhJzsjfy0+zV +NT2TO3kSzXpQ5M2VjOoHPm2fqxD/js+ThDB3QLi4+C7HqakfiTY1lYzXl9/vayt6 +DUD29r9pYL9ErB9tYko2rat54EY7k7Ts6S5jf+8G7Zz234We1APhvqaG +-----END ENCRYPTED PRIVATE KEY----- +` + crypto.keys.import(pem, 'mypassword', (err, key) => { + expect(err).to.not.exist() + expect(key).to.exist() + done() + }) + }) + }) }) From 86a7f8ba0c30a0bedefee7ed8d476b3461718559 Mon Sep 17 00:00:00 2001 From: David Dias Date: Mon, 18 Dec 2017 12:14:15 +0000 Subject: [PATCH 06/11] just to trigger circle with new ubuntu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2de7487f..4ddb827b 100644 --- a/README.md +++ b/README.md @@ -208,4 +208,4 @@ This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/c ## License -[MIT](LICENSE) +[MIT](./LICENSE) From d002f625d5dd02d83750e6d4e0312cca2e245bca Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Tue, 19 Dec 2017 10:49:25 +1300 Subject: [PATCH 07/11] feat: get the RSA key id --- package.json | 1 + src/keys/rsa-class.js | 20 ++++++++++++++++++++ test/keys/rsa.spec.js | 15 ++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f204e417..2810dc2c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "asn1.js": "^5.0.0", "async": "^2.6.0", "browserify-aes": "^1.1.1", + "bs58": "^4.0.1", "jsrsasign": "^8.0.4", "keypair": "^1.0.1", "libp2p-crypto-secp256k1": "~0.2.2", diff --git a/src/keys/rsa-class.js b/src/keys/rsa-class.js index f7ea2c00..d1fcc2ac 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -2,6 +2,7 @@ const multihashing = require('multihashing-async') const protobuf = require('protons') +const bs58 = require('bs58') const crypto = require('./rsa') const pbm = protobuf(require('./keys.proto')) @@ -92,6 +93,25 @@ class RsaPrivateKey { multihashing(this.bytes, 'sha2-256', callback) } + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @param {function(Error, id)} callback + * @returns {undefined} + */ + id (callback) { + this.public.hash((err, hash) => { + if (err) { + return callback(err) + } + callback(null, bs58.encode(hash)) + }) + } + /** * Exports the key into a password protected PEM format * diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index 917ee9b6..69060ab4 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -80,6 +80,15 @@ describe('RSA', function () { }) }) + it('key id', (done) => { + key.id((err, id) => { + expect(err).to.not.exist() + expect(id).to.exist() + expect(id).to.be.a('string') + done() + }) + }) + describe('key equals', () => { it('equals itself', () => { expect(key.equals(key)).to.eql(true) @@ -245,7 +254,11 @@ gnjREs10u7zyqBIZH7KYVgyh27WxLr859ap8cKAH6Fb+UOPtZo3sUeeume60aebn crypto.keys.import(pem, '', (err, key) => { expect(err).to.not.exist() expect(key).to.exist() - done() + key.id((err, id) => { + expect(err).to.not.exist() + expect(id).to.equal('QmfWu2Xp8DZzCkZZzoPB9rcrq4R4RZid6AWE6kmrUAzuHy') + done() + }) }) }) From 8dd2bfeb8c15ae2237580caf591b05c6c53b4d09 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Tue, 19 Dec 2017 11:05:30 +1300 Subject: [PATCH 08/11] feat: get the ED 25519 key id --- src/keys/ed25519-class.js | 20 ++++++++++++++++++++ test/keys/ed25519.spec.js | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/src/keys/ed25519-class.js b/src/keys/ed25519-class.js index fed20edf..0dff4b0b 100644 --- a/src/keys/ed25519-class.js +++ b/src/keys/ed25519-class.js @@ -2,6 +2,7 @@ const multihashing = require('multihashing-async') const protobuf = require('protons') +const bs58 = require('bs58') const crypto = require('./ed25519') const pbm = protobuf(require('./keys.proto')) @@ -77,6 +78,25 @@ class Ed25519PrivateKey { ensure(callback) multihashing(this.bytes, 'sha2-256', callback) } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @param {function(Error, id)} callback + * @returns {undefined} + */ + id (callback) { + this.public.hash((err, hash) => { + if (err) { + return callback(err) + } + callback(null, bs58.encode(hash)) + }) + } } function unmarshalEd25519PrivateKey (bytes, callback) { diff --git a/test/keys/ed25519.spec.js b/test/keys/ed25519.spec.js index 5a12ccf1..b803de64 100644 --- a/test/keys/ed25519.spec.js +++ b/test/keys/ed25519.spec.js @@ -117,6 +117,15 @@ describe('ed25519', function () { }) }) + it('key id', (done) => { + key.id((err, id) => { + expect(err).to.not.exist() + expect(id).to.exist() + expect(id).to.be.a('string') + done() + }) + }) + describe('key equals', () => { it('equals itself', () => { expect( From 8d6eb41569048ce04ebc8ac2ed61f5e8c8f4ccde Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Tue, 19 Dec 2017 16:04:52 +1300 Subject: [PATCH 09/11] feat: pbkdf2 --- README.md | 10 ++++++++++ src/index.js | 1 + src/pbkdf2.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/crypto.spec.js | 26 ++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/pbkdf2.js diff --git a/README.md b/README.md index 4ddb827b..2c56618a 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,16 @@ Converts a PEM password protected private key into its representative object. Generates a Buffer with length `number` populated by random bytes. +### `crypto.pbkdf2(password, salt, iterations, keySize, hash)` + +- `password: String` +- `salt: String` +- `iterations: Number` +- `keySize: Number` in bytes +- `hash: String` the hashing algorithm ('sha1', 'sha2-512', ...) + +Computes the Password Based Key Derivation Function 2; returning a new password. + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)! diff --git a/src/index.js b/src/index.js index 64b9605e..e0a52d74 100644 --- a/src/index.js +++ b/src/index.js @@ -10,3 +10,4 @@ exports.aes = aes exports.hmac = hmac exports.keys = keys exports.randomBytes = require('./random-bytes') +exports.pbkdf2 = require('./pbkdf2') diff --git a/src/pbkdf2.js b/src/pbkdf2.js new file mode 100644 index 00000000..be5a1d42 --- /dev/null +++ b/src/pbkdf2.js @@ -0,0 +1,40 @@ +'use strict' + +const crypto = require('jsrsasign').CryptoJS + +/** + * Maps an IPFS hash name to its jsrsasign equivalent. + * + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @private + */ +const hashName = { + sha1: crypto.algo.SHA1, + 'sha2-256': crypto.algo.SHA256, + 'sha2-512': crypto.algo.SHA512 +} + +/** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {string} password + * @param {string} salt + * @param {number} iterations + * @param {number} keySize (in bytes) + * @param {string} hash - The hash name ('sha1', 'sha2-512, ...) + * @returns {string} - A new password + */ +function pbkdf2 (password, salt, iterations, keySize, hash) { + const opts = { + iterations: iterations, + keySize: keySize / 4, // convert bytes to words (32 bits) + hasher: hashName[hash] + } + if (!opts.hasher) { + throw new Error(`Hash '${hash}' is unknown or not supported`) + } + return crypto.PBKDF2(password, salt, opts).toString() +} + +module.exports = pbkdf2 diff --git a/test/crypto.spec.js b/test/crypto.spec.js index c2a22aa7..a52e2602 100644 --- a/test/crypto.spec.js +++ b/test/crypto.spec.js @@ -107,6 +107,32 @@ describe('libp2p-crypto', function () { }) }) + describe('pbkdf2', () => { + it('generates a derived password using sha1', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha1') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates a derived password using sha2-512', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha2-512') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates the same derived password with the same options', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p2 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p3 = crypto.pbkdf2('password', 'at least 16 character salt', 11, 512 / 8, 'sha1') + expect(p2).to.equal(p1) + expect(p3).to.not.equal(p2) + }) + + it('throws on invalid hash name', () => { + expect(() => crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'shaX-xxx')).to.throw() + }) + }) + describe('randomBytes', () => { it('throws with no number passed', () => { expect(() => { From 7172a115c70bee82969b0d5dffdda4eab2781db2 Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Wed, 20 Dec 2017 17:52:21 +1300 Subject: [PATCH 10/11] fix(pbkdf2): base64 has more chars to guess than hex --- src/pbkdf2.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pbkdf2.js b/src/pbkdf2.js index be5a1d42..cd61a49a 100644 --- a/src/pbkdf2.js +++ b/src/pbkdf2.js @@ -34,7 +34,8 @@ function pbkdf2 (password, salt, iterations, keySize, hash) { if (!opts.hasher) { throw new Error(`Hash '${hash}' is unknown or not supported`) } - return crypto.PBKDF2(password, salt, opts).toString() + const words = crypto.PBKDF2(password, salt, opts) + return crypto.enc.Base64.stringify(words) } module.exports = pbkdf2 From 22f6bfa1e575023a468c28fa8a165fc2d70982fc Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 20 Dec 2017 08:10:30 +0000 Subject: [PATCH 11/11] chore: update deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2810dc2c..f23f0318 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,13 @@ "libp2p-crypto-secp256k1": "~0.2.2", "multihashing-async": "~0.4.7", "pem-jwk": "^1.5.1", - "protons": "^1.0.0", + "protons": "^1.0.1", "rsa-pem-to-jwk": "^1.1.3", "tweetnacl": "^1.0.0", "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" }, "devDependencies": { - "aegir": "^12.2.0", + "aegir": "^12.3.0", "benchmark": "^2.1.4", "chai": "^4.1.2", "chai-string": "^1.4.0",