From 45c2e6762425d83cdce20bb87da63f7a63ea5916 Mon Sep 17 00:00:00 2001 From: Ilya Chesnokov Date: Tue, 26 Nov 2019 22:06:49 +0700 Subject: [PATCH] Use native Node crypto for RSA encryption (#1006) --- src/crypto/crypto.js | 38 ++-- src/crypto/public_key/rsa.js | 179 +++++++++++++----- .../public_key_encrypted_session_key.js | 26 +-- test/crypto/crypto.js | 20 +- test/crypto/rsa.js | 39 ++++ 5 files changed, 196 insertions(+), 106 deletions(-) diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index cbc8d3b0..74f91e85 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -41,6 +41,8 @@ import type_mpi from '../type/mpi'; import type_oid from '../type/oid'; import enums from '../enums'; import util from '../util'; +import pkcs1 from './pkcs1'; +import pkcs5 from './pkcs5'; function constructParams(types, data) { return types.map(function(type, i) { @@ -59,7 +61,7 @@ export default { * @param {Array} pub_params Algorithm-specific public key parameters - * @param {module:type/mpi} data Data to be encrypted as MPI + * @param {String} data Data to be encrypted * @param {String} fingerprint Recipient fingerprint * @returns {Array} encrypted session key parameters @@ -70,13 +72,14 @@ export default { switch (algo) { case enums.publicKey.rsa_encrypt: case enums.publicKey.rsa_encrypt_sign: { - const m = data.toBN(); - const n = pub_params[0].toBN(); - const e = pub_params[1].toBN(); - const res = await publicKey.rsa.encrypt(m, n, e); + data = util.str_to_Uint8Array(data); + const n = pub_params[0].toUint8Array(); + const e = pub_params[1].toUint8Array(); + const res = await publicKey.rsa.encrypt(data, n, e); return constructParams(types, [res]); } case enums.publicKey.elgamal: { + data = new type_mpi(await pkcs1.eme.encode(data, pub_params[0].byteLength())); const m = data.toBN(); const p = pub_params[0].toBN(); const g = pub_params[1].toBN(); @@ -85,6 +88,7 @@ export default { return constructParams(types, [res.c1, res.c2]); } case enums.publicKey.ecdh: { + data = new type_mpi(pkcs5.encode(data)); const oid = pub_params[0]; const Q = pub_params[1].toUint8Array(); const kdf_params = pub_params[2]; @@ -108,20 +112,20 @@ export default { module:type/ecdh_symkey>} data_params encrypted session key parameters * @param {String} fingerprint Recipient fingerprint - * @returns {BN} A BN containing the decrypted data + * @returns {String} String containing the decrypted data * @async */ publicKeyDecrypt: async function(algo, key_params, data_params, fingerprint) { switch (algo) { case enums.publicKey.rsa_encrypt_sign: case enums.publicKey.rsa_encrypt: { - const c = data_params[0].toBN(); - const n = key_params[0].toBN(); // n = pq - const e = key_params[1].toBN(); - const d = key_params[2].toBN(); // de = 1 mod (p-1)(q-1) - const p = key_params[3].toBN(); - const q = key_params[4].toBN(); - const u = key_params[5].toBN(); // p^-1 mod q + const c = data_params[0].toUint8Array(); + const n = key_params[0].toUint8Array(); // n = pq + const e = key_params[1].toUint8Array(); + const d = key_params[2].toUint8Array(); // de = 1 mod (p-1)(q-1) + const p = key_params[3].toUint8Array(); + const q = key_params[4].toUint8Array(); + const u = key_params[5].toUint8Array(); // p^-1 mod q return publicKey.rsa.decrypt(c, n, e, d, p, q, u); } case enums.publicKey.elgamal: { @@ -129,7 +133,8 @@ export default { const c2 = data_params[1].toBN(); const p = key_params[0].toBN(); const x = key_params[3].toBN(); - return publicKey.elgamal.decrypt(c1, c2, p, x); + const result = new type_mpi(await publicKey.elgamal.decrypt(c1, c2, p, x)); + return pkcs1.eme.decode(result.toString()); } case enums.publicKey.ecdh: { const oid = key_params[0]; @@ -138,8 +143,9 @@ export default { const C = data_params[1].data; const Q = key_params[1].toUint8Array(); const d = key_params[3].toUint8Array(); - return publicKey.elliptic.ecdh.decrypt( - oid, kdf_params.cipher, kdf_params.hash, V, C, Q, d, fingerprint); + const result = new type_mpi(await publicKey.elliptic.ecdh.decrypt( + oid, kdf_params.cipher, kdf_params.hash, V, C, Q, d, fingerprint)); + return pkcs5.decode(result.toString()); } default: throw new Error('Invalid public key encryption algorithm.'); diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index 87705e17..44c1165e 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -32,6 +32,7 @@ import config from '../../config'; import util from '../../util'; import pkcs1 from '../pkcs1'; import enums from '../../enums'; +import type_mpi from '../../type/mpi'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -132,62 +133,36 @@ export default { /** * Encrypt message - * @param {BN} m message - * @param {BN} n RSA public modulus - * @param {BN} e RSA public exponent - * @returns {BN} RSA Ciphertext + * @param {Uint8Array} data message + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @returns {Uint8Array} RSA Ciphertext * @async */ - encrypt: async function(m, n, e) { - if (n.cmp(m) <= 0) { - throw new Error('Message size cannot exceed modulus size'); + encrypt: async function(data, n, e) { + if (nodeCrypto) { + return this.nodeEncrypt(data, n, e); } - const nred = new BN.red(n); - return m.toRed(nred).redPow(e).toArrayLike(Uint8Array, 'be', n.byteLength()); + return this.bnEncrypt(data, n, e); }, /** * Decrypt RSA message - * @param {BN} m message - * @param {BN} n RSA public modulus - * @param {BN} e RSA public exponent - * @param {BN} d RSA private exponent - * @param {BN} p RSA private prime p - * @param {BN} q RSA private prime q - * @param {BN} u RSA private coefficient - * @returns {BN} RSA Plaintext + * @param {Uint8Array} m message + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @param {Uint8Array} d RSA private exponent + * @param {Uint8Array} p RSA private prime p + * @param {Uint8Array} q RSA private prime q + * @param {Uint8Array} u RSA private coefficient + * @returns {String} RSA Plaintext * @async */ - decrypt: async function(m, n, e, d, p, q, u) { - if (n.cmp(m) <= 0) { - throw new Error('Data too large.'); + decrypt: async function(data, n, e, d, p, q, u) { + if (nodeCrypto) { + return this.nodeDecrypt(data, n, e, d, p, q, u); } - const dq = d.mod(q.subn(1)); // d mod (q-1) - const dp = d.mod(p.subn(1)); // d mod (p-1) - const pred = new BN.red(p); - const qred = new BN.red(q); - const nred = new BN.red(n); - - let blinder; - let unblinder; - if (config.rsa_blinding) { - unblinder = (await random.getRandomBN(new BN(2), n)).toRed(nred); - blinder = unblinder.redInvm().redPow(e); - m = m.toRed(nred).redMul(blinder).fromRed(); - } - - const mp = m.toRed(pred).redPow(dp); - const mq = m.toRed(qred).redPow(dq); - const t = mq.redSub(mp.fromRed().toRed(qred)); - const h = u.toRed(qred).redMul(t).fromRed(); - - let result = h.mul(p).add(mp).toRed(nred); - - if (config.rsa_blinding) { - result = result.redMul(unblinder); - } - - return result.toArrayLike(Uint8Array, 'be', n.byteLength()); + return this.bnDecrypt(data, n, e, d, p, q, u); }, /** @@ -323,11 +298,12 @@ export default { }, webSign: async function (hash_name, data, n, e, d, p, q, u) { - // OpenPGP keys require that p < q, and Safari Web Crypto requires that p > q. - // We swap them in privateToJwk, so it usually works out, but nevertheless, - // not all OpenPGP keys are compatible with this requirement. - // OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still - // does if the underlying Web Crypto does so (e.g. old MS Edge 50% of the time). + /** OpenPGP keys require that p < q, and Safari Web Crypto requires that p > q. + * We swap them in privateToJwk, so it usually works out, but nevertheless, + * not all OpenPGP keys are compatible with this requirement. + * OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still + * does if the underlying Web Crypto does so (e.g. old MS Edge 50% of the time). + */ const jwk = privateToJwk(n, e, d, p, q, u); const algo = { name: "RSASSA-PKCS1-v1_5", @@ -417,6 +393,107 @@ export default { } }, + nodeEncrypt: async function (data, n, e) { + const keyObject = { + modulus: new BN(n), + publicExponent: new BN(e) + }; + let key; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { + const der = RSAPublicKey.encode(keyObject, 'der'); + key = { key: der, format: 'der', type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } else { + const pem = RSAPublicKey.encode(keyObject, 'pem', { + label: 'RSA PUBLIC KEY' + }); + key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } + return new Uint8Array(nodeCrypto.publicEncrypt(key, data)); + }, + + bnEncrypt: async function (data, n, e) { + n = new BN(n); + data = new type_mpi(await pkcs1.eme.encode(util.Uint8Array_to_str(data), n.byteLength())); + data = data.toBN(); + e = new BN(e); + if (n.cmp(data) <= 0) { + throw new Error('Message size cannot exceed modulus size'); + } + const nred = new BN.red(n); + return data.toRed(nred).redPow(e).toArrayLike(Uint8Array, 'be', n.byteLength()); + }, + + nodeDecrypt: function (data, n, e, d, p, q, u) { + const pBNum = new BN(p); + const qBNum = new BN(q); + const dBNum = new BN(d); + const dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) + const dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) + const keyObject = { + version: 0, + modulus: new BN(n), + publicExponent: new BN(e), + privateExponent: new BN(d), + // switch p and q + prime1: new BN(q), + prime2: new BN(p), + // switch dp and dq + exponent1: dq, + exponent2: dp, + coefficient: new BN(u) + }; + let key; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { + const der = RSAPrivateKey.encode(keyObject, 'der'); + key = { key: der, format: 'der' , type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } else { + const pem = RSAPrivateKey.encode(keyObject, 'pem', { + label: 'RSA PRIVATE KEY' + }); + key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } + return util.Uint8Array_to_str(nodeCrypto.privateDecrypt(key, data)); + }, + + bnDecrypt: async function(data, n, e, d, p, q, u) { + data = new BN(data); + n = new BN(n); + e = new BN(e); + d = new BN(d); + p = new BN(p); + q = new BN(q); + u = new BN(u); + if (n.cmp(data) <= 0) { + throw new Error('Data too large.'); + } + const dq = d.mod(q.subn(1)); // d mod (q-1) + const dp = d.mod(p.subn(1)); // d mod (p-1) + const pred = new BN.red(p); + const qred = new BN.red(q); + const nred = new BN.red(n); + + let blinder; + let unblinder; + if (config.rsa_blinding) { + unblinder = (await random.getRandomBN(new BN(2), n)).toRed(nred); + blinder = unblinder.redInvm().redPow(e); + data = data.toRed(nred).redMul(blinder).fromRed(); + } + + const mp = data.toRed(pred).redPow(dp); + const mq = data.toRed(qred).redPow(dq); + const t = mq.redSub(mp.fromRed().toRed(qred)); + const h = u.toRed(qred).redMul(t).fromRed(); + + let result = h.mul(p).add(mp).toRed(nred); + + if (config.rsa_blinding) { + result = result.redMul(unblinder); + } + + return pkcs1.eme.decode((new type_mpi(result)).toString()); + }, + prime: prime }; diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 8bab822c..81ece181 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -24,7 +24,6 @@ */ import type_keyid from '../type/keyid'; -import type_mpi from '../type/mpi'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -112,17 +111,9 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { data += util.Uint8Array_to_str(this.sessionKey); data += util.Uint8Array_to_str(util.write_checksum(this.sessionKey)); - - let toEncrypt; const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); - if (algo === enums.publicKey.ecdh) { - toEncrypt = new type_mpi(crypto.pkcs5.encode(data)); - } else { - toEncrypt = new type_mpi(await crypto.pkcs1.eme.encode(data, key.params[0].byteLength())); - } - this.encrypted = await crypto.publicKeyEncrypt( - algo, key.params, toEncrypt, key.getFingerprintBytes()); + algo, key.params, data, key.getFingerprintBytes()); return true; }; @@ -137,19 +128,8 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { */ PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) { const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); - const result = new type_mpi(await crypto.publicKeyDecrypt( - algo, key.params, this.encrypted, key.getFingerprintBytes())); - - let checksum; - let decoded; - if (algo === enums.publicKey.ecdh) { - decoded = crypto.pkcs5.decode(result.toString()); - checksum = util.str_to_Uint8Array(decoded.substr(decoded.length - 2)); - } else { - decoded = crypto.pkcs1.eme.decode(result.toString()); - checksum = result.toUint8Array().slice(result.byteLength() - 2); - } - + const decoded = await crypto.publicKeyDecrypt(algo, key.params, this.encrypted, key.getFingerprintBytes()); + const checksum = util.str_to_Uint8Array(decoded.substr(decoded.length - 2)); key = util.str_to_Uint8Array(decoded.substring(1, decoded.length - 2)); if (!util.equalsUint8Array(checksum, util.write_checksum(key))) { diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index eaca2c60..33a4ba3a 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -356,38 +356,26 @@ describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); - return crypto.pkcs1.eme.encode(symmKey, RSApubMPIs[0].byteLength()).then(RSAUnencryptedData => { - const RSAUnencryptedMPI = new openpgp.MPI(RSAUnencryptedData); - return crypto.publicKeyEncrypt(1, RSApubMPIs, RSAUnencryptedMPI); - }).then(RSAEncryptedData => { - + crypto.publicKeyEncrypt(1, RSApubMPIs, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( 1, RSApubMPIs.concat(RSAsecMPIs), RSAEncryptedData ).then(data => { data = new openpgp.MPI(data).write(); data = util.Uint8Array_to_str(data.subarray(2, data.length)); - - const result = crypto.pkcs1.eme.decode(data, RSApubMPIs[0].byteLength()); - expect(result).to.equal(symmKey); + expect(data).to.equal(symmKey); }); }); }); it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); - return crypto.pkcs1.eme.encode(symmKey, ElgamalpubMPIs[0].byteLength()).then(ElgamalUnencryptedData => { - const ElgamalUnencryptedMPI = new openpgp.MPI(ElgamalUnencryptedData); - return crypto.publicKeyEncrypt(16, ElgamalpubMPIs, ElgamalUnencryptedMPI); - }).then(ElgamalEncryptedData => { - + crypto.publicKeyEncrypt(16, ElgamalpubMPIs, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( 16, ElgamalpubMPIs.concat(ElgamalsecMPIs), ElgamalEncryptedData ).then(data => { data = new openpgp.MPI(data).write(); data = util.Uint8Array_to_str(data.subarray(2, data.length)); - - const result = crypto.pkcs1.eme.decode(data, ElgamalpubMPIs[0].byteLength()); - expect(result).to.equal(symmKey); + expect(data).to.equal(symmKey); }); }); }); diff --git a/test/crypto/rsa.js b/test/crypto/rsa.js index e9a5bdad..31666e6f 100644 --- a/test/crypto/rsa.js +++ b/test/crypto/rsa.js @@ -38,6 +38,45 @@ const native = openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(); expect(verify).to.be.true; }); + it('encrypt and decrypt using generated key params', async function() { + const bits = openpgp.util.getWebCryptoAll() ? 2048 : 1024; + const keyParams = await openpgp.crypto.generateParams(openpgp.enums.publicKey.rsa_sign, bits); + const n = keyParams[0].toUint8Array(); + const e = keyParams[1].toUint8Array(); + const d = keyParams[2].toUint8Array(); + const p = keyParams[3].toUint8Array(); + const q = keyParams[4].toUint8Array(); + const u = keyParams[5].toUint8Array(); + const message = openpgp.util.Uint8Array_to_str(await openpgp.crypto.generateSessionKey('aes256')); + const encrypted = await openpgp.crypto.publicKey.rsa.encrypt(openpgp.util.str_to_Uint8Array(message), n, e); + const result = new openpgp.MPI(encrypted); + const decrypted = await openpgp.crypto.publicKey.rsa.decrypt(result.toUint8Array(), n, e, d, p, q, u); + expect(decrypted).to.be.equal(message); + }); + + it('decrypt nodeCrypto by bnCrypto and vice versa', async function() { + if (!openpgp.util.getNodeCrypto()) { + this.skip(); + } + const bits = 1024; + const keyParams = await openpgp.crypto.generateParams(openpgp.enums.publicKey.rsa_sign, bits); + const n = keyParams[0].toUint8Array(); + const e = keyParams[1].toUint8Array(); + const d = keyParams[2].toUint8Array(); + const p = keyParams[3].toUint8Array(); + const q = keyParams[4].toUint8Array(); + const u = keyParams[5].toUint8Array(); + const message = openpgp.util.Uint8Array_to_str(await openpgp.crypto.generateSessionKey('aes256')); + const encryptedBn = await openpgp.crypto.publicKey.rsa.bnEncrypt(openpgp.util.str_to_Uint8Array(message), n, e); + const resultBN = new openpgp.MPI(encryptedBn); + const decrypted1 = await openpgp.crypto.publicKey.rsa.nodeDecrypt(resultBN.toUint8Array(), n, e, d, p, q, u); + expect(decrypted1).to.be.equal(message); + const encryptedNode = await openpgp.crypto.publicKey.rsa.nodeEncrypt(openpgp.util.str_to_Uint8Array(message), n, e); + const resultNode = new openpgp.MPI(encryptedNode); + const decrypted2 = await openpgp.crypto.publicKey.rsa.bnDecrypt(resultNode.toUint8Array(), n, e, d, p, q, u); + expect(decrypted2).to.be.equal(message); + }); + it('compare webCrypto and bn math sign', async function() { if (!openpgp.util.getWebCrypto()) { this.skip();