diff --git a/.aegir.js b/.aegir.js new file mode 100644 index 00000000..25fbe20f --- /dev/null +++ b/.aegir.js @@ -0,0 +1,3 @@ +module.exports = { + bundlesize: { maxSize: '155kB' } +} diff --git a/.travis.yml b/.travis.yml index 88040e7f..00e0e5fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: include: - stage: check script: - - npx aegir commitlint --travis + - npx aegir build --bundlesize - npx aegir dep-check - npm run lint diff --git a/package.json b/package.json index 7137d1d7..ad300c5d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", "coverage": "aegir coverage --ignore src/keys/keys.proto.js", - "size": "bundlesize -f dist/index.min.js -s 139kB" + "size": "aegir build --bundlesize" }, "keywords": [ "IPFS", @@ -36,7 +36,7 @@ "license": "MIT", "dependencies": { "asmcrypto.js": "^2.3.2", - "asn1.js": "^5.0.1", + "asn1.js": "^5.2.0", "bn.js": "^5.0.0", "browserify-aes": "^1.2.0", "bs58": "^4.0.1", @@ -44,22 +44,21 @@ "iso-random-stream": "^1.1.0", "keypair": "^1.0.1", "libp2p-crypto-secp256k1": "~0.4.0", - "multihashing-async": "~0.7.0", - "node-forge": "~0.8.5", + "multihashing-async": "~0.8.0", + "node-forge": "~0.9.1", "pem-jwk": "^2.0.0", "protons": "^1.0.1", "rsa-pem-to-jwk": "^1.1.3", "tweetnacl": "^1.0.1", - "ursa-optional": "~0.10.0" + "ursa-optional": "~0.10.1" }, "devDependencies": { - "aegir": "^19.0.5", + "aegir": "^20.4.1", "benchmark": "^2.1.4", - "bundlesize": "~0.18.0", "chai": "^4.2.0", "chai-string": "^1.5.0", "dirty-chai": "^2.0.1", - "sinon": "^7.3.2" + "sinon": "^7.5.0" }, "engines": { "node": ">=10.0.0", diff --git a/src/keys/index.js b/src/keys/index.js index 205d1086..7775c71f 100644 --- a/src/keys/index.js +++ b/src/keys/index.js @@ -25,7 +25,7 @@ const ErrMissingSecp256K1 = { } function typeToKey (type) { - let key = supportedKeys[type.toLowerCase()] + const key = supportedKeys[type.toLowerCase()] if (!key) { const supported = Object.keys(supportedKeys).join(' / ') throw errcode(new Error(`invalid or unsupported key type ${type}. Must be ${supported}`), 'ERR_UNSUPPORTED_KEY_TYPE') diff --git a/src/keys/jwk2pem.js b/src/keys/jwk2pem.js new file mode 100644 index 00000000..f50791be --- /dev/null +++ b/src/keys/jwk2pem.js @@ -0,0 +1,42 @@ +'use strict' + +const forge = { + util: require('node-forge/lib/util'), + pki: require('node-forge/lib/pki'), + jsbn: require('node-forge/lib/jsbn') +} + +function base64urlToBigInteger (str) { + var bytes = forge.util.decode64( + (str + '==='.slice((str.length + 3) % 4)) + .replace(/-/g, '+') + .replace(/_/g, '/')) + return new forge.jsbn.BigInteger(forge.util.bytesToHex(bytes), 16) +} + +function convert (key, types) { + return types.map(t => base64urlToBigInteger(key[t])) +} + +function jwk2priv (key) { + return forge.pki.setRsaPrivateKey(...convert(key, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'])) +} + +function jwk2privPem (key) { + return forge.pki.privateKeyToPem(jwk2priv(key)) +} + +function jwk2pub (key) { + return forge.pki.setRsaPublicKey(...convert(key, ['n', 'e'])) +} + +function jwk2pubPem (key) { + return forge.pki.publicKeyToPem(jwk2pub(key)) +} + +module.exports = { + jwk2pub, + jwk2pubPem, + jwk2priv, + jwk2privPem +} diff --git a/src/keys/key-stretcher.js b/src/keys/key-stretcher.js index cca81781..c9c53665 100644 --- a/src/keys/key-stretcher.js +++ b/src/keys/key-stretcher.js @@ -29,7 +29,7 @@ module.exports = async (cipherType, hash, secret) => { } if (!hash) { - throw errcode(new Error(`missing hash type`), 'ERR_MISSING_HASH_TYPE') + throw errcode(new Error('missing hash type'), 'ERR_MISSING_HASH_TYPE') } const cipherKeySize = cipher.keySize @@ -41,7 +41,7 @@ module.exports = async (cipherType, hash, secret) => { const m = await hmac.create(hash, secret) let a = await m.digest(seed) - let result = [] + const result = [] let j = 0 while (j < resultLength) { diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-browser.js index be268fb1..ffdd646a 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -120,3 +120,32 @@ function derivePublicFromPrivate (jwKey) { ['verify'] ) } + +/* + +RSA encryption/decryption for the browser with webcrypto workarround +"bloody dark magic. webcrypto's why." + +Explanation: + - Convert JWK to nodeForge + - Convert msg buffer to nodeForge buffer: ByteBuffer is a "binary-string backed buffer", so let's make our buffer a binary string + - Convert resulting nodeForge buffer to buffer: it returns a binary string, turn that into a uint8array(buffer) + +*/ + +const { jwk2pub, jwk2priv } = require('./jwk2pem') + +function convertKey (key, pub, msg, handle) { + const fkey = pub ? jwk2pub(key) : jwk2priv(key) + const fmsg = Buffer.from(msg).toString('binary') + const fomsg = handle(fmsg, fkey) + return Buffer.from(fomsg, 'binary') +} + +exports.encrypt = function (key, msg) { + return convertKey(key, true, msg, (msg, key) => key.encrypt(msg)) +} + +exports.decrypt = function (key, msg) { + return convertKey(key, false, msg, (msg, key) => key.decrypt(msg)) +} diff --git a/src/keys/rsa-class.js b/src/keys/rsa-class.js index 84ad38bd..bc9289ab 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -32,7 +32,7 @@ class RsaPublicKey { } encrypt (bytes) { - return this._key.encrypt(bytes, 'RSAES-PKCS1-V1_5') + return crypto.encrypt(this._key, bytes) } equals (key) { @@ -68,6 +68,10 @@ class RsaPrivateKey { return new RsaPublicKey(this._publicKey) } + decrypt (bytes) { + return crypto.decrypt(this._key, bytes) + } + marshal () { return crypto.utils.jwkToPkcs1(this._key) } diff --git a/src/keys/rsa.js b/src/keys/rsa.js index 019760a7..06605a32 100644 --- a/src/keys/rsa.js +++ b/src/keys/rsa.js @@ -68,3 +68,13 @@ exports.hashAndVerify = async function (key, sig, msg) { // eslint-disable-line const pem = jwkToPem(key) return verify.verify(pem, sig) } + +const padding = crypto.constants.RSA_PKCS1_PADDING + +exports.encrypt = function (key, bytes) { + return crypto.publicEncrypt({ key: jwkToPem(key), padding }, bytes) +} + +exports.decrypt = function (key, bytes) { + return crypto.privateDecrypt({ key: jwkToPem(key), padding }, bytes) +} diff --git a/src/util.js b/src/util.js index e5f7b6b1..3624bfe8 100644 --- a/src/util.js +++ b/src/util.js @@ -6,7 +6,7 @@ const BN = require('asn1.js').bignum // Adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C exports.toBase64 = function toBase64 (bn, len) { // if len is defined then the bytes are leading-0 padded to the length - let s = bn.toArrayLike(Buffer, 'be', len).toString('base64') + const s = bn.toArrayLike(Buffer, 'be', len).toString('base64') return s .replace(/(=*)$/, '') // Remove any trailing '='s diff --git a/test/helpers/test-garbage-error-handling.js b/test/helpers/test-garbage-error-handling.js index 324789c3..75d8ab22 100644 --- a/test/helpers/test-garbage-error-handling.js +++ b/test/helpers/test-garbage-error-handling.js @@ -14,7 +14,7 @@ function doTests (fncName, fnc, num, skipBuffersAndStrings) { // skip this garbage because it's a buffer or a string and we were told do do that return } - let args = [] + const args = [] for (let i = 0; i < num; i++) { args.push(garbage) } diff --git a/test/keys/ephemeral-keys.spec.js b/test/keys/ephemeral-keys.spec.js index 5ad66452..57fcb00e 100644 --- a/test/keys/ephemeral-keys.spec.js +++ b/test/keys/ephemeral-keys.spec.js @@ -61,7 +61,7 @@ describe('generateEphemeralKeyPair', () => { }) }) - it(`handles bad curve name`, async () => { + it('handles bad curve name', async () => { try { await crypto.keys.generateEphemeralKeyPair('bad name') } catch (err) { diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index 175b1443..22be3bc1 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -80,6 +80,26 @@ describe('RSA', function () { expect(valid).to.be.eql(true) }) + it('encrypt and decrypt', async () => { + const data = Buffer.from('hello world') + const enc = await key.public.encrypt(data) + const dec = await key.decrypt(enc) + expect(dec).to.be.eql(data) + }) + + it('encrypt decrypt browser/node interop', async () => { + const id = await crypto.keys.unmarshalPrivateKey(Buffer.from('CAASqAkwggSkAgEAAoIBAQCk0O+6oNRxhcdZe2GxEDrFBkDV4TZFZnp2ly/dL1cGMBql/8oXPZgei6h7+P5zzfDq2YCfwbjbf0IVY1AshRl6B5VGE1WS+9p1y1OZxJf5os6V1ENnTi6FTcyuBl4BN8dmIKOif0hqgqflaT5OhfYZDXfbJyVQj4vb2+Stu2Xpph3nwqAnTw/7GC/7jrt2Cq6Tu1PoZi36wSwEPYW3eQ1HAYxZjTYYDXl2iyHygnTcbkGRwAQ7vjk+mW7u60zyoolCm9f6Y7c/orJ33DDUocbaGJLlHcfd8bioBwaZy/2m7q43X8pQs0Q1/iwUt0HHZj1YARmHKbh0zR31ciFiV37dAgMBAAECggEADtJBNKnA4QKURj47r0YT2uLwkqtBi6UnDyISalQXAdXyl4n0nPlrhBewC5H9I+HZr+zmTbeIjaiYgz7el1pSy7AB4v7bG7AtWZlyx6mvtwHGjR+8/f3AXjl8Vgv5iSeAdXUq8fJ7SyS7v3wi38HZOzCEXj9bci6ud5ODMYJgLE4gZD0+i1+/V9cpuYfGpS/gLTLEMQLiw/9o8NSZ7sAnxg0UlYhotqaQY23hvXPBOe+0oa95zl2n6XTxCafa3dQl/B6CD1tUq9dhbQew4bxqMq/mhRO9pREEqZ083Uh+u4PTc1BeHgIQaS864pHPb+AY1F7KDvPtHhdojnghp8d70QKBgQDeRYFxo6sd04ohY86Z/i9icVYIyCvfXAKnaMKeGUjK7ou6sDJwFX8W97+CzXpZ/vffsk/l5GGhC50KqrITxHAy/h5IjyDODfps7NMIp0Dm9sO4PWibbw3OOVBRc8w3b3i7I8MrUUA1nLHE1T1HA1rKOTz5jYhE0fi9XKiT1ciKOQKBgQC903w+n9y7M7eaMW7Z5/13kZ7PS3HlM681eaPrk8J4J+c6miFF40/8HOsmarS38v0fgTeKkriPz5A7aLzRHhSiOnp350JNM6c3sLwPEs2qx/CRuWWx1rMERatfDdUH6mvlK6QHu0QgSfQR27EO6a6XvVSJXbvFmimjmtIaz/IpxQKBgQDWJ9HYVAGC81abZTaiWK3/A4QJYhQjWNuVwPICsgnYvI4Uib+PDqcs0ffLZ38DRw48kek5bxpBuJbOuDhro1EXUJCNCJpq7jzixituovd9kTRyR3iKii2bDM2+LPwOTXDdnk9lZRugjCEbrPkleq33Ob7uEtfAty4aBTTHe6uEwQKBgQCB+2q8RyMSXNuADhFlzOFXGrOwJm0bEUUMTPrduRQUyt4e1qOqA3klnXe3mqGcxBpnlEe/76/JacvNom6Ikxx16a0qpYRU8OWz0KU1fR6vrrEgV98241k5t6sdL4+MGA1Bo5xyXtzLb1hdUh3vpDwVU2OrnC+To3iXus/b5EBiMQKBgEI1OaBcFiyjgLGEyFKoZbtzH1mdatTExfrAQqCjOVjQByoMpGhHTXwEaosvyYu63Pa8AJPT7juSGaiKYEJFcXO9BiNyVfmQiqSHJcYeuh+fmO9IlHRHgy5xaIIC00AHS2vC/gXwmXAdPis6BZqDJeiCuOLWJ94QXn8JBT8IgGAI', 'base64')) + + const msg = Buffer.from('hello') + + // browser + const dec1 = await id.decrypt(Buffer.from('YRFUDx8UjbWSfDS84cDA4WowaaOmd1qFNAv5QutodCKYb9uPtU/tDiAvJzOGu5DCJRo2J0l/35P2weiB4/C2Cb1aZgXKMx/QQC+2jSJiymhqcZaYerjTvkCFwkjCaqthoVo/YXxsaFZ1q7bdTZUDH1TaJR7hWfSyzyPcA8c0w43MIsw16pY8ZaPSclvnCwhoTg1JGjMk6te3we7+wR8QU7VrPhs54mZWxrpu3NQ8xZ6xQqIedsEiNhBUccrCSzYghgsP0Ae/8iKyGyl3U6IegsJNn8jcocvzOJrmU03rgIFPjvuBdaqB38xDSTjbA123KadB28jNoSZh18q/yH3ZIg==', 'base64')) + expect(dec1).to.be.eql(msg) + // node + const dec2 = await id.decrypt(Buffer.from('e6yxssqXsWc27ozDy0PGKtMkCS28KwFyES2Ijz89yiz+w6bSFkNOhHPKplpPzgQEuNoUGdbseKlJFyRYHjIT8FQFBHZM8UgSkgoimbY5on4xSxXs7E5/+twjqKdB7oNveTaTf7JCwaeUYnKSjbiYFEawtMiQE91F8sTT7TmSzOZ48tUhnddAAZ3Ac/O3Z9MSAKOCDipi+JdZtXRT8KimGt36/7hjjosYmPuHR1Xy/yMTL6SMbXtBM3yAuEgbQgP+q/7kHMHji3/JvTpYdIUU+LVtkMusXNasRA+UWG2zAht18vqjFMsm9JTiihZw9jRHD4vxAhf75M992tnC+0ZuQg==', 'base64')) + expect(dec2).to.be.eql(msg) + }) + it('fails to verify for different data', async () => { const data = Buffer.from('hello world') const sig = await key.sign(data) diff --git a/test/util.spec.js b/test/util.spec.js index bf66ccd7..3688861b 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -24,7 +24,7 @@ describe('Util', () => { }) it('toBase64 zero padding', (done) => { - let bnpad = new BN('ff', 16) + const bnpad = new BN('ff', 16) expect(util.toBase64(bnpad, 2)).to.eql('AP8') done() })