From a5e05603ef1befa827f70f8dd2c2efd4049c34c2 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 17 Jul 2019 11:17:18 +0100 Subject: [PATCH] fix: better error for missing web crypto This PR simply detects missing web crypto and throws an error with an appropriate message. This is a stepping stone that will help users understand the problem until we have time to do a refactor of this module and of all the modules that use it to enable optionally passing your own crypto implementation. refs https://github.com/libp2p/js-libp2p-crypto/pull/149 refs https://github.com/libp2p/js-libp2p-crypto/pull/150 refs https://github.com/libp2p/js-libp2p-crypto/issues/105 refs https://github.com/ipfs/js-ipfs/issues/2153 refs https://github.com/ipfs/js-ipfs/issues/2017 License: MIT Signed-off-by: Alan Shaw --- README.md | 23 ++++++++++++++- src/hmac/index-browser.js | 6 ++-- src/keys/ecdh-browser.js | 13 ++++----- src/keys/rsa-browser.js | 20 ++++++------- src/webcrypto.js | 23 +++++++++++++-- test/browser.js | 61 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 test/browser.js diff --git a/README.md b/README.md index 8c84f65d..00f94865 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,32 @@ 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 some `libp2p-crypto` functions in the browser when the page is served over HTTP.** + ## 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/hmac/index-browser.js b/src/hmac/index-browser.js index 3592291d..1eeb7dfc 100644 --- a/src/hmac/index-browser.js +++ b/src/hmac/index-browser.js @@ -1,6 +1,6 @@ 'use strict' -const webcrypto = require('../webcrypto.js') +const webcrypto = require('../webcrypto') const lengths = require('./lengths') const hashTypes = { @@ -10,13 +10,13 @@ const hashTypes = { } const sign = async (key, data) => { - return Buffer.from(await webcrypto.subtle.sign({ name: 'HMAC' }, key, data)) + return Buffer.from(await webcrypto.get().subtle.sign({ name: 'HMAC' }, key, data)) } exports.create = async function (hashType, secret) { const hash = hashTypes[hashType] - const key = await webcrypto.subtle.importKey( + const key = await webcrypto.get().subtle.importKey( 'raw', secret, { diff --git a/src/keys/ecdh-browser.js b/src/keys/ecdh-browser.js index 4c4134de..48f08433 100644 --- a/src/keys/ecdh-browser.js +++ b/src/keys/ecdh-browser.js @@ -1,7 +1,7 @@ 'use strict' const errcode = require('err-code') -const webcrypto = require('../webcrypto.js') +const webcrypto = require('../webcrypto') const BN = require('asn1.js').bignum const { toBase64, toBn } = require('../util') const validateCurveType = require('./validate-curve-type') @@ -14,8 +14,7 @@ const bits = { exports.generateEphmeralKeyPair = async function (curve) { validateCurveType(Object.keys(bits), curve) - - const pair = await webcrypto.subtle.generateKey( + const pair = await webcrypto.get().subtle.generateKey( { name: 'ECDH', namedCurve: curve @@ -29,7 +28,7 @@ exports.generateEphmeralKeyPair = async function (curve) { let privateKey if (forcePrivate) { - privateKey = await webcrypto.subtle.importKey( + privateKey = await webcrypto.get().subtle.importKey( 'jwk', unmarshalPrivateKey(curve, forcePrivate), { @@ -44,7 +43,7 @@ exports.generateEphmeralKeyPair = async function (curve) { } const keys = [ - await webcrypto.subtle.importKey( + await webcrypto.get().subtle.importKey( 'jwk', unmarshalPublicKey(curve, theirPub), { @@ -57,7 +56,7 @@ exports.generateEphmeralKeyPair = async function (curve) { privateKey ] - return Buffer.from(await webcrypto.subtle.deriveBits( + return Buffer.from(await webcrypto.get().subtle.deriveBits( { name: 'ECDH', namedCurve: curve, @@ -68,7 +67,7 @@ exports.generateEphmeralKeyPair = async function (curve) { )) } - const publicKey = await webcrypto.subtle.exportKey('jwk', pair.publicKey) + const publicKey = await webcrypto.get().subtle.exportKey('jwk', pair.publicKey) return { key: marshalPublicKey(publicKey), diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-browser.js index 5cefbdb3..be268fb1 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -1,12 +1,12 @@ 'use strict' -const webcrypto = require('../webcrypto.js') +const webcrypto = require('../webcrypto') const randomBytes = require('../random-bytes') exports.utils = require('./rsa-utils') exports.generateKey = async function (bits) { - const pair = await webcrypto.subtle.generateKey( + const pair = await webcrypto.get().subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: bits, @@ -27,7 +27,7 @@ exports.generateKey = async function (bits) { // Takes a jwk key exports.unmarshalPrivateKey = async function (key) { - const privateKey = await webcrypto.subtle.importKey( + const privateKey = await webcrypto.get().subtle.importKey( 'jwk', key, { @@ -57,7 +57,7 @@ exports.unmarshalPrivateKey = async function (key) { exports.getRandomValues = randomBytes exports.hashAndSign = async function (key, msg) { - const privateKey = await webcrypto.subtle.importKey( + const privateKey = await webcrypto.get().subtle.importKey( 'jwk', key, { @@ -68,7 +68,7 @@ exports.hashAndSign = async function (key, msg) { ['sign'] ) - const sig = await webcrypto.subtle.sign( + const sig = await webcrypto.get().subtle.sign( { name: 'RSASSA-PKCS1-v1_5' }, privateKey, Uint8Array.from(msg) @@ -78,7 +78,7 @@ exports.hashAndSign = async function (key, msg) { } exports.hashAndVerify = async function (key, sig, msg) { - const publicKey = await webcrypto.subtle.importKey( + const publicKey = await webcrypto.get().subtle.importKey( 'jwk', key, { @@ -89,7 +89,7 @@ exports.hashAndVerify = async function (key, sig, msg) { ['verify'] ) - return webcrypto.subtle.verify( + return webcrypto.get().subtle.verify( { name: 'RSASSA-PKCS1-v1_5' }, publicKey, sig, @@ -99,13 +99,13 @@ exports.hashAndVerify = async function (key, sig, msg) { 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..1663369a 100644 --- a/src/webcrypto.js +++ b/src/webcrypto.js @@ -1,5 +1,24 @@ -/* 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). +exports.get = (win = self) => { + const nativeCrypto = win.crypto || win.msCrypto + + if (!nativeCrypto || !nativeCrypto.subtle) { + throw 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' } + ) + } + + return nativeCrypto +} diff --git a/test/browser.js b/test/browser.js new file mode 100644 index 00000000..264c41a6 --- /dev/null +++ b/test/browser.js @@ -0,0 +1,61 @@ +/* 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') + +async function expectMissingWebCrypto (fn) { + try { + await fn() + } catch (err) { + expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO') + return + } + throw new Error('Expected missing web crypto error') +} + +describe('Missing web crypto', () => { + let webcryptoGet + let rsaPrivateKey + + before(async () => { + rsaPrivateKey = await crypto.keys.generateKeyPair('RSA', 512) + }) + + before(() => { + webcryptoGet = webcrypto.get + webcrypto.get = () => webcryptoGet({}) + }) + + after(() => { + webcrypto.get = webcryptoGet + }) + + it('should error for hmac create when web crypto is missing', () => { + return expectMissingWebCrypto(() => crypto.hmac.create('SHA256', Buffer.from('secret'))) + }) + + it('should error for generate ephemeral key pair when web crypto is missing', () => { + return expectMissingWebCrypto(() => crypto.keys.generateEphemeralKeyPair('P-256')) + }) + + it('should error for generate rsa key pair when web crypto is missing', () => { + return expectMissingWebCrypto(() => crypto.keys.generateKeyPair('rsa', 256)) + }) + + it('should error for unmarshal RSA private key when web crypto is missing', () => { + return expectMissingWebCrypto(() => crypto.keys.unmarshalPrivateKey(crypto.keys.marshalPrivateKey(rsaPrivateKey))) + }) + + it('should error for sign RSA private key when web crypto is missing', () => { + return expectMissingWebCrypto(() => rsaPrivateKey.sign(Buffer.from('test'))) + }) + + it('should error for verify RSA public key when web crypto is missing', () => { + return expectMissingWebCrypto(() => rsaPrivateKey.public.verify(Buffer.from('test'), Buffer.from('test'))) + }) +})