From 357593f1effc8a7daf37bf28b754e2a73375a3ab Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Jul 2019 14:10:17 +0100 Subject: [PATCH] feat: bring your own web crypto Changes `webcrypto.js` to check for native web crypto availability and falls back to using `window.__crypto` if not available. If the user wants to bring their own Web Crypto API compatible implementation then they simply need to assign it to `window.__crypto` before they start using IPFS. Checks are done in the functions that require web crypto to give the user the flexibility to assign to `window.__crypto` before OR after they import `libp2p-crypto`. It also means that users have the ability to use other exported functions that do not require web crypto without having to worry about sorting their own implementation. We use `window.__crypto` because `window.crypto` is a readonly property in secure context and always readonly in workers. If `window.crypto` and `window.__cypto` are unavailable then an appropriate error message is reported to the user with a `ERR_MISSING_WEB_CRYPTO` code. I've also added documentation to the README. This is a backwards compatible change. closes https://github.com/libp2p/js-libp2p-crypto/pull/149 resolves https://github.com/libp2p/js-libp2p-crypto/issues/105 resolves https://github.com/ipfs/js-ipfs/issues/2017 resolves https://github.com/ipfs/js-ipfs/issues/2153 License: MIT Signed-off-by: Alan Shaw --- README.md | 31 +++++++++++++- src/errors.js | 10 +++++ src/hmac/index-browser.js | 10 ++++- src/keys/ecdh-browser.js | 16 +++++--- src/keys/rsa-browser.js | 36 ++++++++++++---- src/webcrypto.js | 10 ++++- test/browser.js | 86 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 src/errors.js create mode 100644 test/browser.js diff --git a/README.md b/README.md index 54b84c8a..4b3efb25 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,40 @@ This repo contains the JavaScript implementation of the crypto primitives needed npm install --save libp2p-crypto ``` +## Usage + +```js +const crypto = require('libp2p-crypto') + +// Now available to you: +// +// crypto.aes +// crypto.hmac +// crypto.keys +// etc. +// +// See full API details below... +``` + +### Web Crypto API + +The `libp2p-crypto` library depends on the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in the browser. Web Crypto is available in all modern browsers, however browsers restrict its usage to [Secure Contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + +**This means you will not be able to use `libp2p-crypto` in the browser when the page is served over HTTP.** + +You can either move your page to be served over HTTPS or bring your own Web Crypto API implementation. If `libp2p-crypto` does not find the official Web Crypto API at `window.crypto` (or `self.crypto`) it will use `window.__crypto`. + +```js +if (!window.isSecureContext) { + window.__crypto = // ... your Web Crypto API compatible implementation +} +``` + ## API ### `crypto.aes` -Expoes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197. +Exposes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197. This uses `CTR` mode. diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 00000000..7bbc71f9 --- /dev/null +++ b/src/errors.js @@ -0,0 +1,10 @@ +exports.ERR_MISSING_WEB_CRYPTO = () => Object.assign( + new Error( + 'Missing Web Crypto API. ' + + 'The most likely cause of this error is that this page is being accessed ' + + 'from an insecure context (i.e. not HTTPS). For more information and ' + + 'possible resolutions see ' + + 'https://github.com/libp2p/js-libp2p-crypto/blob/master/README.md#web-crypto-api' + ), + { code: 'ERR_MISSING_WEB_CRYPTO' } +) diff --git a/src/hmac/index-browser.js b/src/hmac/index-browser.js index c3c40c33..f576f4b7 100644 --- a/src/hmac/index-browser.js +++ b/src/hmac/index-browser.js @@ -4,6 +4,8 @@ const nodeify = require('../nodeify') const crypto = require('../webcrypto') const lengths = require('./lengths') +const nextTick = require('async/nextTick') +const { ERR_MISSING_WEB_CRYPTO } = require('../errors') const hashTypes = { SHA1: 'SHA-1', @@ -12,14 +14,18 @@ const hashTypes = { } const sign = (key, data, cb) => { - nodeify(crypto.subtle.sign({ name: 'HMAC' }, key, data) + nodeify(crypto.get().subtle.sign({ name: 'HMAC' }, key, data) .then((raw) => Buffer.from(raw)), cb) } exports.create = function (hashType, secret, callback) { + if (!crypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + const hash = hashTypes[hashType] - nodeify(crypto.subtle.importKey( + nodeify(crypto.get().subtle.importKey( 'raw', secret, { diff --git a/src/keys/ecdh-browser.js b/src/keys/ecdh-browser.js index ab54759a..2900b621 100644 --- a/src/keys/ecdh-browser.js +++ b/src/keys/ecdh-browser.js @@ -3,6 +3,8 @@ const webcrypto = require('../webcrypto') const nodeify = require('../nodeify') const BN = require('asn1.js').bignum +const nextTick = require('async/nextTick') +const { ERR_MISSING_WEB_CRYPTO } = require('../errors') const util = require('../util') const toBase64 = util.toBase64 @@ -15,7 +17,11 @@ const bits = { } exports.generateEphmeralKeyPair = function (curve, callback) { - nodeify(webcrypto.subtle.generateKey( + if (!webcrypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + + nodeify(webcrypto.get().subtle.generateKey( { name: 'ECDH', namedCurve: curve @@ -33,7 +39,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) { let privateKey if (forcePrivate) { - privateKey = webcrypto.subtle.importKey( + privateKey = webcrypto.get().subtle.importKey( 'jwk', unmarshalPrivateKey(curve, forcePrivate), { @@ -48,7 +54,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) { } const keys = Promise.all([ - webcrypto.subtle.importKey( + webcrypto.get().subtle.importKey( 'jwk', unmarshalPublicKey(curve, theirPub), { @@ -61,7 +67,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) { privateKey ]) - nodeify(keys.then((keys) => webcrypto.subtle.deriveBits( + nodeify(keys.then((keys) => webcrypto.get().subtle.deriveBits( { name: 'ECDH', namedCurve: curve, @@ -72,7 +78,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) { )).then((bits) => Buffer.from(bits)), cb) } - return webcrypto.subtle.exportKey('jwk', pair.publicKey) + return webcrypto.get().subtle.exportKey('jwk', pair.publicKey) .then((publicKey) => { return { key: marshalPublicKey(publicKey), diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-browser.js index 6cf12865..107764ec 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -3,11 +3,17 @@ const nodeify = require('../nodeify') const webcrypto = require('../webcrypto') const randomBytes = require('../random-bytes') +const nextTick = require('async/nextTick') +const { ERR_MISSING_WEB_CRYPTO } = require('../errors') exports.utils = require('./rsa-utils') exports.generateKey = function (bits, callback) { - nodeify(webcrypto.subtle.generateKey( + if (!webcrypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + + nodeify(webcrypto.get().subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: bits, @@ -26,7 +32,11 @@ exports.generateKey = function (bits, callback) { // Takes a jwk key exports.unmarshalPrivateKey = function (key, callback) { - const privateKey = webcrypto.subtle.importKey( + if (!webcrypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + + const privateKey = webcrypto.get().subtle.importKey( 'jwk', key, { @@ -52,7 +62,11 @@ exports.unmarshalPrivateKey = function (key, callback) { exports.getRandomValues = randomBytes exports.hashAndSign = function (key, msg, callback) { - nodeify(webcrypto.subtle.importKey( + if (!webcrypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + + nodeify(webcrypto.get().subtle.importKey( 'jwk', key, { @@ -62,7 +76,7 @@ exports.hashAndSign = function (key, msg, callback) { false, ['sign'] ).then((privateKey) => { - return webcrypto.subtle.sign( + return webcrypto.get().subtle.sign( { name: 'RSASSA-PKCS1-v1_5' }, privateKey, Uint8Array.from(msg) @@ -71,7 +85,11 @@ exports.hashAndSign = function (key, msg, callback) { } exports.hashAndVerify = function (key, sig, msg, callback) { - nodeify(webcrypto.subtle.importKey( + if (!webcrypto.get()) { + return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO())) + } + + nodeify(webcrypto.get().subtle.importKey( 'jwk', key, { @@ -81,7 +99,7 @@ exports.hashAndVerify = function (key, sig, msg, callback) { false, ['verify'] ).then((publicKey) => { - return webcrypto.subtle.verify( + return webcrypto.get().subtle.verify( { name: 'RSASSA-PKCS1-v1_5' }, publicKey, sig, @@ -92,13 +110,13 @@ exports.hashAndVerify = function (key, sig, msg, callback) { function exportKey (pair) { return Promise.all([ - webcrypto.subtle.exportKey('jwk', pair.privateKey), - webcrypto.subtle.exportKey('jwk', pair.publicKey) + webcrypto.get().subtle.exportKey('jwk', pair.privateKey), + webcrypto.get().subtle.exportKey('jwk', pair.publicKey) ]) } function derivePublicFromPrivate (jwKey) { - return webcrypto.subtle.importKey( + return webcrypto.get().subtle.importKey( 'jwk', { kty: jwKey.kty, diff --git a/src/webcrypto.js b/src/webcrypto.js index 0f9a557b..fce7ea17 100644 --- a/src/webcrypto.js +++ b/src/webcrypto.js @@ -1,5 +1,11 @@ -/* global self */ +/* eslint-env browser */ 'use strict' -module.exports = self.crypto || self.msCrypto +// Check native crypto exists and is enabled (In insecure context `self.crypto` +// exists but `self.crypto.subtle` does not). Fallback to custom Web Crypto API +// compatible implementation at `self.__crypto` if no native. +exports.get = (win = self) => { + const nativeCrypto = win.crypto || win.msCrypto + return nativeCrypto && nativeCrypto.subtle ? nativeCrypto : win.__crypto +} diff --git a/test/browser.js b/test/browser.js new file mode 100644 index 00000000..88a22e7f --- /dev/null +++ b/test/browser.js @@ -0,0 +1,86 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const crypto = require('../') +const webcrypto = require('../src/webcrypto') + +describe('Missing web crypto', () => { + let webcryptoGet + let rsaPrivateKey + + before(done => { + crypto.keys.generateKeyPair('RSA', 512, (err, key) => { + if (err) return done(err) + rsaPrivateKey = key + done() + }) + }) + + before(() => { + webcryptoGet = webcrypto.get + webcrypto.get = () => null + }) + + after(() => { + webcrypto.get = webcryptoGet + }) + + it('should error for hmac create when web crypto is missing', done => { + crypto.hmac.create('SHA256', Buffer.from('secret'), err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) + + it('should error for generate ephemeral key pair when web crypto is missing', done => { + crypto.keys.generateEphemeralKeyPair('P-256', err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) + + it('should error for generate rsa key pair when web crypto is missing', done => { + crypto.keys.generateKeyPair('rsa', 256, err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) + + it('should error for unmarshal RSA private key when web crypto is missing', done => { + crypto.keys.unmarshalPrivateKey(crypto.keys.marshalPrivateKey(rsaPrivateKey), err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) + + it('should error for sign RSA private key when web crypto is missing', done => { + rsaPrivateKey.sign(Buffer.from('test'), err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) + + it('should error for verify RSA public key when web crypto is missing', done => { + rsaPrivateKey.public.verify(Buffer.from('test'), Buffer.from('test'), err => { + expect(err).to.exist() + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + done() + }) + }) +}) + +describe('BYO web crypto', () => { + it('should fallback to self.__crypto if self.crypto is missing', () => { + const customCrypto = {} + expect(webcrypto.get({ __crypto: customCrypto })).to.equal(customCrypto) + }) +})