Use native Node crypto for RSA encryption (#1006)

This commit is contained in:
Ilya Chesnokov 2019-11-26 22:06:49 +07:00 committed by Daniel Huigens
parent 495fe1091c
commit 45c2e67624
5 changed files with 196 additions and 106 deletions

View File

@ -41,6 +41,8 @@ import type_mpi from '../type/mpi';
import type_oid from '../type/oid'; import type_oid from '../type/oid';
import enums from '../enums'; import enums from '../enums';
import util from '../util'; import util from '../util';
import pkcs1 from './pkcs1';
import pkcs5 from './pkcs5';
function constructParams(types, data) { function constructParams(types, data) {
return types.map(function(type, i) { return types.map(function(type, i) {
@ -59,7 +61,7 @@ export default {
* @param {Array<module:type/mpi| * @param {Array<module:type/mpi|
module:type/oid| module:type/oid|
module:type/kdf_params>} pub_params Algorithm-specific public key parameters module:type/kdf_params>} 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 * @param {String} fingerprint Recipient fingerprint
* @returns {Array<module:type/mpi| * @returns {Array<module:type/mpi|
* module:type/ecdh_symkey>} encrypted session key parameters * module:type/ecdh_symkey>} encrypted session key parameters
@ -70,13 +72,14 @@ export default {
switch (algo) { switch (algo) {
case enums.publicKey.rsa_encrypt: case enums.publicKey.rsa_encrypt:
case enums.publicKey.rsa_encrypt_sign: { case enums.publicKey.rsa_encrypt_sign: {
const m = data.toBN(); data = util.str_to_Uint8Array(data);
const n = pub_params[0].toBN(); const n = pub_params[0].toUint8Array();
const e = pub_params[1].toBN(); const e = pub_params[1].toUint8Array();
const res = await publicKey.rsa.encrypt(m, n, e); const res = await publicKey.rsa.encrypt(data, n, e);
return constructParams(types, [res]); return constructParams(types, [res]);
} }
case enums.publicKey.elgamal: { case enums.publicKey.elgamal: {
data = new type_mpi(await pkcs1.eme.encode(data, pub_params[0].byteLength()));
const m = data.toBN(); const m = data.toBN();
const p = pub_params[0].toBN(); const p = pub_params[0].toBN();
const g = pub_params[1].toBN(); const g = pub_params[1].toBN();
@ -85,6 +88,7 @@ export default {
return constructParams(types, [res.c1, res.c2]); return constructParams(types, [res.c1, res.c2]);
} }
case enums.publicKey.ecdh: { case enums.publicKey.ecdh: {
data = new type_mpi(pkcs5.encode(data));
const oid = pub_params[0]; const oid = pub_params[0];
const Q = pub_params[1].toUint8Array(); const Q = pub_params[1].toUint8Array();
const kdf_params = pub_params[2]; const kdf_params = pub_params[2];
@ -108,20 +112,20 @@ export default {
module:type/ecdh_symkey>} module:type/ecdh_symkey>}
data_params encrypted session key parameters data_params encrypted session key parameters
* @param {String} fingerprint Recipient fingerprint * @param {String} fingerprint Recipient fingerprint
* @returns {BN} A BN containing the decrypted data * @returns {String} String containing the decrypted data
* @async * @async
*/ */
publicKeyDecrypt: async function(algo, key_params, data_params, fingerprint) { publicKeyDecrypt: async function(algo, key_params, data_params, fingerprint) {
switch (algo) { switch (algo) {
case enums.publicKey.rsa_encrypt_sign: case enums.publicKey.rsa_encrypt_sign:
case enums.publicKey.rsa_encrypt: { case enums.publicKey.rsa_encrypt: {
const c = data_params[0].toBN(); const c = data_params[0].toUint8Array();
const n = key_params[0].toBN(); // n = pq const n = key_params[0].toUint8Array(); // n = pq
const e = key_params[1].toBN(); const e = key_params[1].toUint8Array();
const d = key_params[2].toBN(); // de = 1 mod (p-1)(q-1) const d = key_params[2].toUint8Array(); // de = 1 mod (p-1)(q-1)
const p = key_params[3].toBN(); const p = key_params[3].toUint8Array();
const q = key_params[4].toBN(); const q = key_params[4].toUint8Array();
const u = key_params[5].toBN(); // p^-1 mod q const u = key_params[5].toUint8Array(); // p^-1 mod q
return publicKey.rsa.decrypt(c, n, e, d, p, q, u); return publicKey.rsa.decrypt(c, n, e, d, p, q, u);
} }
case enums.publicKey.elgamal: { case enums.publicKey.elgamal: {
@ -129,7 +133,8 @@ export default {
const c2 = data_params[1].toBN(); const c2 = data_params[1].toBN();
const p = key_params[0].toBN(); const p = key_params[0].toBN();
const x = key_params[3].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: { case enums.publicKey.ecdh: {
const oid = key_params[0]; const oid = key_params[0];
@ -138,8 +143,9 @@ export default {
const C = data_params[1].data; const C = data_params[1].data;
const Q = key_params[1].toUint8Array(); const Q = key_params[1].toUint8Array();
const d = key_params[3].toUint8Array(); const d = key_params[3].toUint8Array();
return publicKey.elliptic.ecdh.decrypt( const result = new type_mpi(await publicKey.elliptic.ecdh.decrypt(
oid, kdf_params.cipher, kdf_params.hash, V, C, Q, d, fingerprint); oid, kdf_params.cipher, kdf_params.hash, V, C, Q, d, fingerprint));
return pkcs5.decode(result.toString());
} }
default: default:
throw new Error('Invalid public key encryption algorithm.'); throw new Error('Invalid public key encryption algorithm.');

View File

@ -32,6 +32,7 @@ import config from '../../config';
import util from '../../util'; import util from '../../util';
import pkcs1 from '../pkcs1'; import pkcs1 from '../pkcs1';
import enums from '../../enums'; import enums from '../../enums';
import type_mpi from '../../type/mpi';
const webCrypto = util.getWebCrypto(); const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto(); const nodeCrypto = util.getNodeCrypto();
@ -132,62 +133,36 @@ export default {
/** /**
* Encrypt message * Encrypt message
* @param {BN} m message * @param {Uint8Array} data message
* @param {BN} n RSA public modulus * @param {Uint8Array} n RSA public modulus
* @param {BN} e RSA public exponent * @param {Uint8Array} e RSA public exponent
* @returns {BN} RSA Ciphertext * @returns {Uint8Array} RSA Ciphertext
* @async * @async
*/ */
encrypt: async function(m, n, e) { encrypt: async function(data, n, e) {
if (n.cmp(m) <= 0) { if (nodeCrypto) {
throw new Error('Message size cannot exceed modulus size'); return this.nodeEncrypt(data, n, e);
} }
const nred = new BN.red(n); return this.bnEncrypt(data, n, e);
return m.toRed(nred).redPow(e).toArrayLike(Uint8Array, 'be', n.byteLength());
}, },
/** /**
* Decrypt RSA message * Decrypt RSA message
* @param {BN} m message * @param {Uint8Array} m message
* @param {BN} n RSA public modulus * @param {Uint8Array} n RSA public modulus
* @param {BN} e RSA public exponent * @param {Uint8Array} e RSA public exponent
* @param {BN} d RSA private exponent * @param {Uint8Array} d RSA private exponent
* @param {BN} p RSA private prime p * @param {Uint8Array} p RSA private prime p
* @param {BN} q RSA private prime q * @param {Uint8Array} q RSA private prime q
* @param {BN} u RSA private coefficient * @param {Uint8Array} u RSA private coefficient
* @returns {BN} RSA Plaintext * @returns {String} RSA Plaintext
* @async * @async
*/ */
decrypt: async function(m, n, e, d, p, q, u) { decrypt: async function(data, n, e, d, p, q, u) {
if (n.cmp(m) <= 0) { if (nodeCrypto) {
throw new Error('Data too large.'); return this.nodeDecrypt(data, n, e, d, p, q, u);
} }
const dq = d.mod(q.subn(1)); // d mod (q-1) return this.bnDecrypt(data, n, e, d, p, q, u);
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());
}, },
/** /**
@ -323,11 +298,12 @@ export default {
}, },
webSign: async function (hash_name, data, n, e, d, p, q, u) { 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. /** 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, * We swap them in privateToJwk, so it usually works out, but nevertheless,
// not all OpenPGP keys are compatible with this requirement. * not all OpenPGP keys are compatible with this requirement.
// OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still * 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). * 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 jwk = privateToJwk(n, e, d, p, q, u);
const algo = { const algo = {
name: "RSASSA-PKCS1-v1_5", 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 prime: prime
}; };

View File

@ -24,7 +24,6 @@
*/ */
import type_keyid from '../type/keyid'; import type_keyid from '../type/keyid';
import type_mpi from '../type/mpi';
import crypto from '../crypto'; import crypto from '../crypto';
import enums from '../enums'; import enums from '../enums';
import util from '../util'; 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(this.sessionKey);
data += util.Uint8Array_to_str(util.write_checksum(this.sessionKey)); data += util.Uint8Array_to_str(util.write_checksum(this.sessionKey));
let toEncrypt;
const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); 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( this.encrypted = await crypto.publicKeyEncrypt(
algo, key.params, toEncrypt, key.getFingerprintBytes()); algo, key.params, data, key.getFingerprintBytes());
return true; return true;
}; };
@ -137,19 +128,8 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) {
*/ */
PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) { PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) {
const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const result = new type_mpi(await crypto.publicKeyDecrypt( const decoded = await crypto.publicKeyDecrypt(algo, key.params, this.encrypted, key.getFingerprintBytes());
algo, key.params, this.encrypted, key.getFingerprintBytes())); const checksum = util.str_to_Uint8Array(decoded.substr(decoded.length - 2));
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);
}
key = util.str_to_Uint8Array(decoded.substring(1, decoded.length - 2)); key = util.str_to_Uint8Array(decoded.substring(1, decoded.length - 2));
if (!util.equalsUint8Array(checksum, util.write_checksum(key))) { if (!util.equalsUint8Array(checksum, util.write_checksum(key))) {

View File

@ -356,38 +356,26 @@ describe('API functional testing', function() {
it('Asymmetric using RSA with eme_pkcs1 padding', function () { it('Asymmetric using RSA with eme_pkcs1 padding', function () {
const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256'));
return crypto.pkcs1.eme.encode(symmKey, RSApubMPIs[0].byteLength()).then(RSAUnencryptedData => { crypto.publicKeyEncrypt(1, RSApubMPIs, symmKey).then(RSAEncryptedData => {
const RSAUnencryptedMPI = new openpgp.MPI(RSAUnencryptedData);
return crypto.publicKeyEncrypt(1, RSApubMPIs, RSAUnencryptedMPI);
}).then(RSAEncryptedData => {
return crypto.publicKeyDecrypt( return crypto.publicKeyDecrypt(
1, RSApubMPIs.concat(RSAsecMPIs), RSAEncryptedData 1, RSApubMPIs.concat(RSAsecMPIs), RSAEncryptedData
).then(data => { ).then(data => {
data = new openpgp.MPI(data).write(); data = new openpgp.MPI(data).write();
data = util.Uint8Array_to_str(data.subarray(2, data.length)); data = util.Uint8Array_to_str(data.subarray(2, data.length));
expect(data).to.equal(symmKey);
const result = crypto.pkcs1.eme.decode(data, RSApubMPIs[0].byteLength());
expect(result).to.equal(symmKey);
}); });
}); });
}); });
it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () {
const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256'));
return crypto.pkcs1.eme.encode(symmKey, ElgamalpubMPIs[0].byteLength()).then(ElgamalUnencryptedData => { crypto.publicKeyEncrypt(16, ElgamalpubMPIs, symmKey).then(ElgamalEncryptedData => {
const ElgamalUnencryptedMPI = new openpgp.MPI(ElgamalUnencryptedData);
return crypto.publicKeyEncrypt(16, ElgamalpubMPIs, ElgamalUnencryptedMPI);
}).then(ElgamalEncryptedData => {
return crypto.publicKeyDecrypt( return crypto.publicKeyDecrypt(
16, ElgamalpubMPIs.concat(ElgamalsecMPIs), ElgamalEncryptedData 16, ElgamalpubMPIs.concat(ElgamalsecMPIs), ElgamalEncryptedData
).then(data => { ).then(data => {
data = new openpgp.MPI(data).write(); data = new openpgp.MPI(data).write();
data = util.Uint8Array_to_str(data.subarray(2, data.length)); data = util.Uint8Array_to_str(data.subarray(2, data.length));
expect(data).to.equal(symmKey);
const result = crypto.pkcs1.eme.decode(data, ElgamalpubMPIs[0].byteLength());
expect(result).to.equal(symmKey);
}); });
}); });
}); });

View File

@ -38,6 +38,45 @@ const native = openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto();
expect(verify).to.be.true; 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() { it('compare webCrypto and bn math sign', async function() {
if (!openpgp.util.getWebCrypto()) { if (!openpgp.util.getWebCrypto()) {
this.skip(); this.skip();