From 92c78f40dcecb882fde97982ea54b683f0f9d707 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 4 Jul 2019 11:17:28 +0100 Subject: [PATCH] fix: crypto in insecure browser context This PR adds `crypto-browserify` to the dependencies and replaces `crypto` with `crypto-browserify` when bundled in the browser. In files that require webcrypto we check to see if it's available. If it is not we require the Node.js implementation (which has `crypto` replaced with `crypto-browserify`) and if it is available then we use the webcrypto version (so we get fast crypto). Shipping `crypto-browserify` adds to the bundle size: Current gzipped size: 142,824 bytes New gzipped size: 214,499 bytes Difference: **+71,675 bytes** It's not an insignificant addition so we need to decide whether this is worth it. If not accepted, we need to add checks when libp2p-crypto methods are called and callback with an appropriate error message. JS IPFS will continue to have issues opened with confusion around this otherwise! See https://github.com/ipfs/js-ipfs/issues/963 https://github.com/ipfs/js-ipfs/issues/964 https://github.com/ipfs/js-ipfs/issues/2153 resolves https://github.com/libp2p/js-libp2p-crypto/issues/105 License: MIT Signed-off-by: Alan Shaw --- package.json | 12 +-- src/aes/ciphers-browser.js | 8 -- src/aes/index-browser.js | 55 ----------- src/hmac/index-crypto.js | 22 +++++ .../{index-browser.js => index-webcrypto.js} | 0 src/hmac/index.js | 21 +--- src/keys/ecdh-crypto.js | 41 ++++++++ .../{ecdh-browser.js => ecdh-webcrypto.js} | 0 src/keys/ecdh.js | 40 +------- src/keys/keypair-browser.js | 1 + src/keys/keypair.js | 24 +++++ src/keys/rsa-crypto.js | 78 +++++++++++++++ src/keys/{rsa-browser.js => rsa-webcrypto.js} | 0 src/keys/rsa.js | 98 +------------------ src/webcrypto.js | 2 +- 15 files changed, 177 insertions(+), 225 deletions(-) delete mode 100644 src/aes/ciphers-browser.js delete mode 100644 src/aes/index-browser.js create mode 100644 src/hmac/index-crypto.js rename src/hmac/{index-browser.js => index-webcrypto.js} (100%) create mode 100644 src/keys/ecdh-crypto.js rename src/keys/{ecdh-browser.js => ecdh-webcrypto.js} (100%) create mode 100644 src/keys/keypair-browser.js create mode 100644 src/keys/keypair.js create mode 100644 src/keys/rsa-crypto.js rename src/keys/{rsa-browser.js => rsa-webcrypto.js} (100%) diff --git a/package.json b/package.json index 5f0c8ceb..ede1a949 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,8 @@ "main": "src/index.js", "leadMaintainer": "Friedel Ziegelmayer ", "browser": { - "./src/hmac/index.js": "./src/hmac/index-browser.js", - "./src/keys/ecdh.js": "./src/keys/ecdh-browser.js", - "./src/aes/ciphers.js": "./src/aes/ciphers-browser.js", - "./src/keys/rsa.js": "./src/keys/rsa-browser.js" + "crypto": "crypto-browserify", + "./src/keys/keypair.js": "./src/keys/keypair-browser.js" }, "files": [ "src", @@ -35,12 +33,10 @@ ], "license": "MIT", "dependencies": { - "asmcrypto.js": "^2.3.2", "asn1.js": "^5.0.1", "async": "^2.6.2", - "bn.js": "^4.11.8", - "browserify-aes": "^1.2.0", "bs58": "^4.0.1", + "crypto-browserify": "^3.12.0", "iso-random-stream": "^1.1.0", "keypair": "^1.0.1", "libp2p-crypto-secp256k1": "~0.3.0", @@ -48,12 +44,12 @@ "node-forge": "~0.7.6", "pem-jwk": "^2.0.0", "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", "tweetnacl": "^1.0.1", "ursa-optional": "~0.9.10" }, "devDependencies": { "aegir": "^18.2.2", + "bn.js": "^4.11.8", "benchmark": "^2.1.4", "bundlesize": "~0.17.1", "chai": "^4.2.0", diff --git a/src/aes/ciphers-browser.js b/src/aes/ciphers-browser.js deleted file mode 100644 index 5aee05ae..00000000 --- a/src/aes/ciphers-browser.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict' - -const crypto = require('browserify-aes') - -module.exports = { - createCipheriv: crypto.createCipheriv, - createDecipheriv: crypto.createDecipheriv -} diff --git a/src/aes/index-browser.js b/src/aes/index-browser.js deleted file mode 100644 index ee3af5d7..00000000 --- a/src/aes/index-browser.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -const asm = require('asmcrypto.js') -const nextTick = require('async/nextTick') - -exports.create = function (key, iv, callback) { - const done = (err, res) => nextTick(() => callback(err, res)) - - if (key.length !== 16 && key.length !== 32) { - return done(new Error('Invalid key length')) - } - - const enc = new asm.AES_CTR.Encrypt({ - key: key, - nonce: iv - }) - const dec = new asm.AES_CTR.Decrypt({ - key: key, - nonce: iv - }) - - const res = { - encrypt (data, cb) { - const done = (err, res) => nextTick(() => cb(err, res)) - - let res - try { - res = Buffer.from( - enc.process(data).result - ) - } catch (err) { - return done(err) - } - - done(null, res) - }, - - decrypt (data, cb) { - const done = (err, res) => nextTick(() => cb(err, res)) - - let res - try { - res = Buffer.from( - dec.process(data).result - ) - } catch (err) { - return done(err) - } - - done(null, res) - } - } - - done(null, res) -} diff --git a/src/hmac/index-crypto.js b/src/hmac/index-crypto.js new file mode 100644 index 00000000..61b61b10 --- /dev/null +++ b/src/hmac/index-crypto.js @@ -0,0 +1,22 @@ +'use strict' + +const crypto = require('crypto') +const lengths = require('./lengths') +const nextTick = require('async/nextTick') + +exports.create = function (hash, secret, callback) { + const res = { + digest (data, cb) { + const hmac = crypto.createHmac(hash.toLowerCase(), secret) + + hmac.update(data) + + nextTick(() => { + cb(null, hmac.digest()) + }) + }, + length: lengths[hash] + } + + callback(null, res) +} diff --git a/src/hmac/index-browser.js b/src/hmac/index-webcrypto.js similarity index 100% rename from src/hmac/index-browser.js rename to src/hmac/index-webcrypto.js diff --git a/src/hmac/index.js b/src/hmac/index.js index 61b61b10..bf0aaa5d 100644 --- a/src/hmac/index.js +++ b/src/hmac/index.js @@ -1,22 +1,5 @@ 'use strict' -const crypto = require('crypto') -const lengths = require('./lengths') -const nextTick = require('async/nextTick') +const webcrypto = require('../webcrypto') -exports.create = function (hash, secret, callback) { - const res = { - digest (data, cb) { - const hmac = crypto.createHmac(hash.toLowerCase(), secret) - - hmac.update(data) - - nextTick(() => { - cb(null, hmac.digest()) - }) - }, - length: lengths[hash] - } - - callback(null, res) -} +module.exports = webcrypto ? require('./index-webcrypto') : require('./index-crypto') diff --git a/src/keys/ecdh-crypto.js b/src/keys/ecdh-crypto.js new file mode 100644 index 00000000..b0fda097 --- /dev/null +++ b/src/keys/ecdh-crypto.js @@ -0,0 +1,41 @@ +'use strict' + +const crypto = require('crypto') +const nextTick = require('async/nextTick') + +const curves = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1' +} + +exports.generateEphmeralKeyPair = function (curve, callback) { + if (!curves[curve]) { + return callback(new Error(`Unkown curve: ${curve}`)) + } + const ecdh = crypto.createECDH(curves[curve]) + ecdh.generateKeys() + + nextTick(() => callback(null, { + key: ecdh.getPublicKey(), + genSharedKey (theirPub, forcePrivate, cb) { + if (typeof forcePrivate === 'function') { + cb = forcePrivate + forcePrivate = null + } + + if (forcePrivate) { + ecdh.setPrivateKey(forcePrivate.private) + } + + let secret + try { + secret = ecdh.computeSecret(theirPub) + } catch (err) { + return cb(err) + } + + nextTick(() => cb(null, secret)) + } + })) +} diff --git a/src/keys/ecdh-browser.js b/src/keys/ecdh-webcrypto.js similarity index 100% rename from src/keys/ecdh-browser.js rename to src/keys/ecdh-webcrypto.js diff --git a/src/keys/ecdh.js b/src/keys/ecdh.js index b0fda097..67a479b1 100644 --- a/src/keys/ecdh.js +++ b/src/keys/ecdh.js @@ -1,41 +1,5 @@ 'use strict' -const crypto = require('crypto') -const nextTick = require('async/nextTick') +const webcrypto = require('../webcrypto') -const curves = { - 'P-256': 'prime256v1', - 'P-384': 'secp384r1', - 'P-521': 'secp521r1' -} - -exports.generateEphmeralKeyPair = function (curve, callback) { - if (!curves[curve]) { - return callback(new Error(`Unkown curve: ${curve}`)) - } - const ecdh = crypto.createECDH(curves[curve]) - ecdh.generateKeys() - - nextTick(() => callback(null, { - key: ecdh.getPublicKey(), - genSharedKey (theirPub, forcePrivate, cb) { - if (typeof forcePrivate === 'function') { - cb = forcePrivate - forcePrivate = null - } - - if (forcePrivate) { - ecdh.setPrivateKey(forcePrivate.private) - } - - let secret - try { - secret = ecdh.computeSecret(theirPub) - } catch (err) { - return cb(err) - } - - nextTick(() => cb(null, secret)) - } - })) -} +module.exports = webcrypto ? require('./ecdh-webcrypto') : require('./ecdh-crypto') diff --git a/src/keys/keypair-browser.js b/src/keys/keypair-browser.js new file mode 100644 index 00000000..704ace43 --- /dev/null +++ b/src/keys/keypair-browser.js @@ -0,0 +1 @@ +module.exports = require('keypair') diff --git a/src/keys/keypair.js b/src/keys/keypair.js new file mode 100644 index 00000000..2d3d1e6a --- /dev/null +++ b/src/keys/keypair.js @@ -0,0 +1,24 @@ +let keypair + +try { + if (process.env.LP2P_FORCE_CRYPTO_LIB === 'keypair') { + throw new Error('Force keypair usage') + } + + const ursa = require('ursa-optional') // throws if not compiled + keypair = ({ bits }) => { + const key = ursa.generatePrivateKey(bits) + return { + private: key.toPrivatePem(), + public: key.toPublicPem() + } + } +} catch (e) { + if (process.env.LP2P_FORCE_CRYPTO_LIB === 'ursa') { + throw e + } + + keypair = require('keypair') +} + +module.exports = keypair diff --git a/src/keys/rsa-crypto.js b/src/keys/rsa-crypto.js new file mode 100644 index 00000000..7eb420ec --- /dev/null +++ b/src/keys/rsa-crypto.js @@ -0,0 +1,78 @@ +'use strict' + +const crypto = require('crypto') +const randomBytes = require('../random-bytes') +const nextTick = require('async/nextTick') +const keypair = require('./keypair') +const pemToJwk = require('pem-jwk').pem2jwk +const jwkToPem = require('pem-jwk').jwk2pem + +exports.utils = require('./rsa-utils') + +exports.generateKey = function (bits, callback) { + nextTick(() => { + let result + try { + const key = keypair({ bits: bits }) + result = { + privateKey: pemToJwk(key.private), + publicKey: pemToJwk(key.public) + } + } catch (err) { + return callback(err) + } + + callback(null, result) + }) +} + +// Takes a jwk key +exports.unmarshalPrivateKey = function (key, callback) { + nextTick(() => { + if (!key) { + return callback(new Error('Key is invalid')) + } + callback(null, { + privateKey: key, + publicKey: { + kty: key.kty, + n: key.n, + e: key.e + } + }) + }) +} + +exports.getRandomValues = randomBytes + +exports.hashAndSign = function (key, msg, callback) { + nextTick(() => { + let result + try { + const sign = crypto.createSign('RSA-SHA256') + sign.update(msg) + const pem = jwkToPem(key) + result = sign.sign(pem) + } catch (err) { + return callback(new Error('Key or message is invalid!: ' + err.message)) + } + + callback(null, result) + }) +} + +exports.hashAndVerify = function (key, sig, msg, callback) { + nextTick(() => { + let result + try { + const verify = crypto.createVerify('RSA-SHA256') + verify.update(msg) + const pem = jwkToPem(key) + result = verify.verify(pem, sig) + } catch (err) { + return callback(new Error('Key or message is invalid!:' + err.message)) + } + + callback(null, result) + }) +} diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-webcrypto.js similarity index 100% rename from src/keys/rsa-browser.js rename to src/keys/rsa-webcrypto.js diff --git a/src/keys/rsa.js b/src/keys/rsa.js index 67ea7665..5d087572 100644 --- a/src/keys/rsa.js +++ b/src/keys/rsa.js @@ -1,99 +1,5 @@ 'use strict' -const crypto = require('crypto') -const randomBytes = require('../random-bytes') -const nextTick = require('async/nextTick') +const webcrypto = require('../webcrypto') -let keypair -try { - if (process.env.LP2P_FORCE_CRYPTO_LIB === 'keypair') { - throw new Error('Force keypair usage') - } - - const ursa = require('ursa-optional') // throws if not compiled - keypair = ({ bits }) => { - const key = ursa.generatePrivateKey(bits) - return { - private: key.toPrivatePem(), - public: key.toPublicPem() - } - } -} catch (e) { - if (process.env.LP2P_FORCE_CRYPTO_LIB === 'ursa') { - throw e - } - - keypair = require('keypair') -} -const pemToJwk = require('pem-jwk').pem2jwk -const jwkToPem = require('pem-jwk').jwk2pem - -exports.utils = require('./rsa-utils') - -exports.generateKey = function (bits, callback) { - nextTick(() => { - let result - try { - const key = keypair({ bits: bits }) - result = { - privateKey: pemToJwk(key.private), - publicKey: pemToJwk(key.public) - } - } catch (err) { - return callback(err) - } - - callback(null, result) - }) -} - -// Takes a jwk key -exports.unmarshalPrivateKey = function (key, callback) { - nextTick(() => { - if (!key) { - return callback(new Error('Key is invalid')) - } - callback(null, { - privateKey: key, - publicKey: { - kty: key.kty, - n: key.n, - e: key.e - } - }) - }) -} - -exports.getRandomValues = randomBytes - -exports.hashAndSign = function (key, msg, callback) { - nextTick(() => { - let result - try { - const sign = crypto.createSign('RSA-SHA256') - sign.update(msg) - const pem = jwkToPem(key) - result = sign.sign(pem) - } catch (err) { - return callback(new Error('Key or message is invalid!: ' + err.message)) - } - - callback(null, result) - }) -} - -exports.hashAndVerify = function (key, sig, msg, callback) { - nextTick(() => { - let result - try { - const verify = crypto.createVerify('RSA-SHA256') - verify.update(msg) - const pem = jwkToPem(key) - result = verify.verify(pem, sig) - } catch (err) { - return callback(new Error('Key or message is invalid!:' + err.message)) - } - - callback(null, result) - }) -} +module.exports = webcrypto ? require('./rsa-webcrypto') : require('./rsa-crypto') diff --git a/src/webcrypto.js b/src/webcrypto.js index 0f9a557b..97cc17d7 100644 --- a/src/webcrypto.js +++ b/src/webcrypto.js @@ -2,4 +2,4 @@ 'use strict' -module.exports = self.crypto || self.msCrypto +module.exports = typeof self === 'undefined' ? null : (self.crypto || self.msCrypto)