Skip to content
This repository was archived by the owner on Jul 21, 2023. It is now read-only.

Commit 357593f

Browse files
author
Alan Shaw
committed
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 #149 resolves #105 resolves ipfs/js-ipfs#2017 resolves ipfs/js-ipfs#2153 License: MIT Signed-off-by: Alan Shaw <alan.shaw@protocol.ai>
1 parent 5cd0e8c commit 357593f

File tree

7 files changed

+180
-19
lines changed

7 files changed

+180
-19
lines changed

README.md

+30-1
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,40 @@ This repo contains the JavaScript implementation of the crypto primitives needed
4848
npm install --save libp2p-crypto
4949
```
5050

51+
## Usage
52+
53+
```js
54+
const crypto = require('libp2p-crypto')
55+
56+
// Now available to you:
57+
//
58+
// crypto.aes
59+
// crypto.hmac
60+
// crypto.keys
61+
// etc.
62+
//
63+
// See full API details below...
64+
```
65+
66+
### Web Crypto API
67+
68+
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).
69+
70+
**This means you will not be able to use `libp2p-crypto` in the browser when the page is served over HTTP.**
71+
72+
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`.
73+
74+
```js
75+
if (!window.isSecureContext) {
76+
window.__crypto = // ... your Web Crypto API compatible implementation
77+
}
78+
```
79+
5180
## API
5281

5382
### `crypto.aes`
5483

55-
Expoes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197.
84+
Exposes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197.
5685

5786
This uses `CTR` mode.
5887

src/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
exports.ERR_MISSING_WEB_CRYPTO = () => Object.assign(
2+
new Error(
3+
'Missing Web Crypto API. ' +
4+
'The most likely cause of this error is that this page is being accessed ' +
5+
'from an insecure context (i.e. not HTTPS). For more information and ' +
6+
'possible resolutions see ' +
7+
'https://github.com/libp2p/js-libp2p-crypto/blob/master/README.md#web-crypto-api'
8+
),
9+
{ code: 'ERR_MISSING_WEB_CRYPTO' }
10+
)

src/hmac/index-browser.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const nodeify = require('../nodeify')
44

55
const crypto = require('../webcrypto')
66
const lengths = require('./lengths')
7+
const nextTick = require('async/nextTick')
8+
const { ERR_MISSING_WEB_CRYPTO } = require('../errors')
79

810
const hashTypes = {
911
SHA1: 'SHA-1',
@@ -12,14 +14,18 @@ const hashTypes = {
1214
}
1315

1416
const sign = (key, data, cb) => {
15-
nodeify(crypto.subtle.sign({ name: 'HMAC' }, key, data)
17+
nodeify(crypto.get().subtle.sign({ name: 'HMAC' }, key, data)
1618
.then((raw) => Buffer.from(raw)), cb)
1719
}
1820

1921
exports.create = function (hashType, secret, callback) {
22+
if (!crypto.get()) {
23+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
24+
}
25+
2026
const hash = hashTypes[hashType]
2127

22-
nodeify(crypto.subtle.importKey(
28+
nodeify(crypto.get().subtle.importKey(
2329
'raw',
2430
secret,
2531
{

src/keys/ecdh-browser.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const webcrypto = require('../webcrypto')
44
const nodeify = require('../nodeify')
55
const BN = require('asn1.js').bignum
6+
const nextTick = require('async/nextTick')
7+
const { ERR_MISSING_WEB_CRYPTO } = require('../errors')
68

79
const util = require('../util')
810
const toBase64 = util.toBase64
@@ -15,7 +17,11 @@ const bits = {
1517
}
1618

1719
exports.generateEphmeralKeyPair = function (curve, callback) {
18-
nodeify(webcrypto.subtle.generateKey(
20+
if (!webcrypto.get()) {
21+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
22+
}
23+
24+
nodeify(webcrypto.get().subtle.generateKey(
1925
{
2026
name: 'ECDH',
2127
namedCurve: curve
@@ -33,7 +39,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) {
3339
let privateKey
3440

3541
if (forcePrivate) {
36-
privateKey = webcrypto.subtle.importKey(
42+
privateKey = webcrypto.get().subtle.importKey(
3743
'jwk',
3844
unmarshalPrivateKey(curve, forcePrivate),
3945
{
@@ -48,7 +54,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) {
4854
}
4955

5056
const keys = Promise.all([
51-
webcrypto.subtle.importKey(
57+
webcrypto.get().subtle.importKey(
5258
'jwk',
5359
unmarshalPublicKey(curve, theirPub),
5460
{
@@ -61,7 +67,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) {
6167
privateKey
6268
])
6369

64-
nodeify(keys.then((keys) => webcrypto.subtle.deriveBits(
70+
nodeify(keys.then((keys) => webcrypto.get().subtle.deriveBits(
6571
{
6672
name: 'ECDH',
6773
namedCurve: curve,
@@ -72,7 +78,7 @@ exports.generateEphmeralKeyPair = function (curve, callback) {
7278
)).then((bits) => Buffer.from(bits)), cb)
7379
}
7480

75-
return webcrypto.subtle.exportKey('jwk', pair.publicKey)
81+
return webcrypto.get().subtle.exportKey('jwk', pair.publicKey)
7682
.then((publicKey) => {
7783
return {
7884
key: marshalPublicKey(publicKey),

src/keys/rsa-browser.js

+27-9
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@
33
const nodeify = require('../nodeify')
44
const webcrypto = require('../webcrypto')
55
const randomBytes = require('../random-bytes')
6+
const nextTick = require('async/nextTick')
7+
const { ERR_MISSING_WEB_CRYPTO } = require('../errors')
68

79
exports.utils = require('./rsa-utils')
810

911
exports.generateKey = function (bits, callback) {
10-
nodeify(webcrypto.subtle.generateKey(
12+
if (!webcrypto.get()) {
13+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
14+
}
15+
16+
nodeify(webcrypto.get().subtle.generateKey(
1117
{
1218
name: 'RSASSA-PKCS1-v1_5',
1319
modulusLength: bits,
@@ -26,7 +32,11 @@ exports.generateKey = function (bits, callback) {
2632

2733
// Takes a jwk key
2834
exports.unmarshalPrivateKey = function (key, callback) {
29-
const privateKey = webcrypto.subtle.importKey(
35+
if (!webcrypto.get()) {
36+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
37+
}
38+
39+
const privateKey = webcrypto.get().subtle.importKey(
3040
'jwk',
3141
key,
3242
{
@@ -52,7 +62,11 @@ exports.unmarshalPrivateKey = function (key, callback) {
5262
exports.getRandomValues = randomBytes
5363

5464
exports.hashAndSign = function (key, msg, callback) {
55-
nodeify(webcrypto.subtle.importKey(
65+
if (!webcrypto.get()) {
66+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
67+
}
68+
69+
nodeify(webcrypto.get().subtle.importKey(
5670
'jwk',
5771
key,
5872
{
@@ -62,7 +76,7 @@ exports.hashAndSign = function (key, msg, callback) {
6276
false,
6377
['sign']
6478
).then((privateKey) => {
65-
return webcrypto.subtle.sign(
79+
return webcrypto.get().subtle.sign(
6680
{ name: 'RSASSA-PKCS1-v1_5' },
6781
privateKey,
6882
Uint8Array.from(msg)
@@ -71,7 +85,11 @@ exports.hashAndSign = function (key, msg, callback) {
7185
}
7286

7387
exports.hashAndVerify = function (key, sig, msg, callback) {
74-
nodeify(webcrypto.subtle.importKey(
88+
if (!webcrypto.get()) {
89+
return nextTick(() => callback(ERR_MISSING_WEB_CRYPTO()))
90+
}
91+
92+
nodeify(webcrypto.get().subtle.importKey(
7593
'jwk',
7694
key,
7795
{
@@ -81,7 +99,7 @@ exports.hashAndVerify = function (key, sig, msg, callback) {
8199
false,
82100
['verify']
83101
).then((publicKey) => {
84-
return webcrypto.subtle.verify(
102+
return webcrypto.get().subtle.verify(
85103
{ name: 'RSASSA-PKCS1-v1_5' },
86104
publicKey,
87105
sig,
@@ -92,13 +110,13 @@ exports.hashAndVerify = function (key, sig, msg, callback) {
92110

93111
function exportKey (pair) {
94112
return Promise.all([
95-
webcrypto.subtle.exportKey('jwk', pair.privateKey),
96-
webcrypto.subtle.exportKey('jwk', pair.publicKey)
113+
webcrypto.get().subtle.exportKey('jwk', pair.privateKey),
114+
webcrypto.get().subtle.exportKey('jwk', pair.publicKey)
97115
])
98116
}
99117

100118
function derivePublicFromPrivate (jwKey) {
101-
return webcrypto.subtle.importKey(
119+
return webcrypto.get().subtle.importKey(
102120
'jwk',
103121
{
104122
kty: jwKey.kty,

src/webcrypto.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
/* global self */
1+
/* eslint-env browser */
22

33
'use strict'
44

5-
module.exports = self.crypto || self.msCrypto
5+
// Check native crypto exists and is enabled (In insecure context `self.crypto`
6+
// exists but `self.crypto.subtle` does not). Fallback to custom Web Crypto API
7+
// compatible implementation at `self.__crypto` if no native.
8+
exports.get = (win = self) => {
9+
const nativeCrypto = win.crypto || win.msCrypto
10+
return nativeCrypto && nativeCrypto.subtle ? nativeCrypto : win.__crypto
11+
}

test/browser.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
const dirtyChai = require('dirty-chai')
6+
const expect = chai.expect
7+
chai.use(dirtyChai)
8+
const crypto = require('../')
9+
const webcrypto = require('../src/webcrypto')
10+
11+
describe('Missing web crypto', () => {
12+
let webcryptoGet
13+
let rsaPrivateKey
14+
15+
before(done => {
16+
crypto.keys.generateKeyPair('RSA', 512, (err, key) => {
17+
if (err) return done(err)
18+
rsaPrivateKey = key
19+
done()
20+
})
21+
})
22+
23+
before(() => {
24+
webcryptoGet = webcrypto.get
25+
webcrypto.get = () => null
26+
})
27+
28+
after(() => {
29+
webcrypto.get = webcryptoGet
30+
})
31+
32+
it('should error for hmac create when web crypto is missing', done => {
33+
crypto.hmac.create('SHA256', Buffer.from('secret'), err => {
34+
expect(err).to.exist()
35+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
36+
done()
37+
})
38+
})
39+
40+
it('should error for generate ephemeral key pair when web crypto is missing', done => {
41+
crypto.keys.generateEphemeralKeyPair('P-256', err => {
42+
expect(err).to.exist()
43+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
44+
done()
45+
})
46+
})
47+
48+
it('should error for generate rsa key pair when web crypto is missing', done => {
49+
crypto.keys.generateKeyPair('rsa', 256, err => {
50+
expect(err).to.exist()
51+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
52+
done()
53+
})
54+
})
55+
56+
it('should error for unmarshal RSA private key when web crypto is missing', done => {
57+
crypto.keys.unmarshalPrivateKey(crypto.keys.marshalPrivateKey(rsaPrivateKey), err => {
58+
expect(err).to.exist()
59+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
60+
done()
61+
})
62+
})
63+
64+
it('should error for sign RSA private key when web crypto is missing', done => {
65+
rsaPrivateKey.sign(Buffer.from('test'), err => {
66+
expect(err).to.exist()
67+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
68+
done()
69+
})
70+
})
71+
72+
it('should error for verify RSA public key when web crypto is missing', done => {
73+
rsaPrivateKey.public.verify(Buffer.from('test'), Buffer.from('test'), err => {
74+
expect(err).to.exist()
75+
expect(err.code).to.equal('ERR_MISSING_WEB_CRYPTO')
76+
done()
77+
})
78+
})
79+
})
80+
81+
describe('BYO web crypto', () => {
82+
it('should fallback to self.__crypto if self.crypto is missing', () => {
83+
const customCrypto = {}
84+
expect(webcrypto.get({ __crypto: customCrypto })).to.equal(customCrypto)
85+
})
86+
})

0 commit comments

Comments
 (0)