From e71fc150c02bce9c9f7dfbe5da676f9469df3dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Fri, 25 Oct 2019 19:01:57 +0200 Subject: [PATCH] feat: backport enc/dec (#160) * feat: add (rsa)pubKey.encrypt and (rsa)privKey.decrypt nodeJS only for now * feat: browser enc/dec * fix: browser rsa enc/dec * refactor: cleanup * fix: lint * fix: use direct buffers instead of converting to hex * fix: padding error * test: add interop test * feat: use forge to convert jwk2forge * fix: jwk var naming * fix: missing cbwrap --- package.json | 2 +- src/keys/jwk2pem.js | 42 +++++++++++++++++++++++++++++++++++++++++ src/keys/rsa-browser.js | 29 ++++++++++++++++++++++++++++ src/keys/rsa-class.js | 19 +++++++++++++++---- src/keys/rsa.js | 10 ++++++++++ test/keys/rsa.spec.js | 35 ++++++++++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 src/keys/jwk2pem.js diff --git a/package.json b/package.json index 6087c5d6..434c1330 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "keypair": "^1.0.1", "libp2p-crypto-secp256k1": "~0.3.0", "multihashing-async": "~0.5.1", - "node-forge": "^0.8.5", + "node-forge": "~0.9.1", "pem-jwk": "^2.0.0", "protons": "^1.0.1", "rsa-pem-to-jwk": "^1.1.3", 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/rsa-browser.js b/src/keys/rsa-browser.js index 6cf12865..e5c03b3c 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -113,3 +113,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 ff67731e..dfb79eba 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -32,8 +32,8 @@ class RsaPublicKey { }) } - encrypt (bytes) { - return this._key.encrypt(bytes, 'RSAES-PKCS1-V1_5') + encrypt (bytes, cb) { + return cbWrap(() => crypto.encrypt(this._key, bytes), cb) } equals (key) { @@ -46,6 +46,17 @@ class RsaPublicKey { } } +function cbWrap (f, cb) { + let res + try { + res = f() + } catch (err) { + cb(err) + } + + return cb(null, res) +} + class RsaPrivateKey { // key - Object of the jwk format // publicKey - Buffer of the spki format @@ -71,8 +82,8 @@ class RsaPrivateKey { return new RsaPublicKey(this._publicKey) } - decrypt (msg, callback) { - crypto.decrypt(this._key, msg, callback) + decrypt (bytes, cb) { + cbWrap(() => crypto.decrypt(this._key, bytes), cb) } marshal () { diff --git a/src/keys/rsa.js b/src/keys/rsa.js index 67ea7665..c73d9ded 100644 --- a/src/keys/rsa.js +++ b/src/keys/rsa.js @@ -97,3 +97,13 @@ exports.hashAndVerify = function (key, sig, msg, callback) { callback(null, result) }) } + +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/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index b46c5f74..1b678bde 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -128,6 +128,41 @@ describe('RSA', function () { }) }) + it('encrypt and decrypt', (done) => { + const data = Buffer.from('hello world') + key.public.encrypt(data, (err, enc) => { + if (err) { return done(err) } + + key.decrypt(enc, (err, dec) => { + if (err) { return done(err) } + expect(dec).to.be.eql(data) + + done() + }) + }) + }) + + it('encrypt decrypt browser/node interop', (done) => { + 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'), (err, id) => { + if (err) { return done(err) } + + const msg = Buffer.from('hello') + // browser + id.decrypt(Buffer.from('YRFUDx8UjbWSfDS84cDA4WowaaOmd1qFNAv5QutodCKYb9uPtU/tDiAvJzOGu5DCJRo2J0l/35P2weiB4/C2Cb1aZgXKMx/QQC+2jSJiymhqcZaYerjTvkCFwkjCaqthoVo/YXxsaFZ1q7bdTZUDH1TaJR7hWfSyzyPcA8c0w43MIsw16pY8ZaPSclvnCwhoTg1JGjMk6te3we7+wR8QU7VrPhs54mZWxrpu3NQ8xZ6xQqIedsEiNhBUccrCSzYghgsP0Ae/8iKyGyl3U6IegsJNn8jcocvzOJrmU03rgIFPjvuBdaqB38xDSTjbA123KadB28jNoSZh18q/yH3ZIg==', 'base64'), (err, dec1) => { + if (err) { return done(err) } + expect(dec1).to.be.eql(msg) + + // node + id.decrypt(Buffer.from('e6yxssqXsWc27ozDy0PGKtMkCS28KwFyES2Ijz89yiz+w6bSFkNOhHPKplpPzgQEuNoUGdbseKlJFyRYHjIT8FQFBHZM8UgSkgoimbY5on4xSxXs7E5/+twjqKdB7oNveTaTf7JCwaeUYnKSjbiYFEawtMiQE91F8sTT7TmSzOZ48tUhnddAAZ3Ac/O3Z9MSAKOCDipi+JdZtXRT8KimGt36/7hjjosYmPuHR1Xy/yMTL6SMbXtBM3yAuEgbQgP+q/7kHMHji3/JvTpYdIUU+LVtkMusXNasRA+UWG2zAht18vqjFMsm9JTiihZw9jRHD4vxAhf75M992tnC+0ZuQg==', 'base64'), (err, dec2) => { + if (err) { return done(err) } + expect(dec2).to.be.eql(msg) + + done() + }) + }) + }) + }) + it('fails to verify for different data', (done) => { const data = Buffer.from('hello world') key.sign(data, (err, sig) => {