/* * Copyright 2018 Scytl Secure Electronic Voting SA * * All rights reserved * * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package */ /* jshint node:true */ 'use strict'; var Policy = require('scytl-cryptopolicy'); var validator = require('./input-validator'); var codec = require('scytl-codec'); var forge = require('node-forge'); module.exports = AsymmetricEncrypter; /** * @class AsymmetricEncrypter * @classdesc The asymmetric encrypter API. To instantiate this object, use the * method {@link AsymmetricCryptographyService.newEncrypter}. * @hideconstructor * @param {Policy} * policy The cryptographic policy to use. * @param {SecureRandomService} * secureRandomService The secure random service to use. */ function AsymmetricEncrypter(policy, secureRandomService) { // PRIVATE /////////////////////////////////////////////////////////////////// var algorithm_; var oaepDigester_; var oeapMaskDigester_; var kemDeriver_; var randomGenerator_; var forgePublicKey_; function initForgeCipher(algorithm) { if (algorithm.name === Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name) { oaepDigester_ = getRsaOaepHash(algorithm.hashAlgorithm); if (algorithm.maskGenerator.name === Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.maskGenerator.MGF1 .name) { oeapMaskDigester_ = getRsaOaepMaskHash(algorithm.maskGenerator.hashAlgorithm); } else { throw new Error( 'Asymmetric encrypter RSA-OAEP mask generation function \'' + algorithm.maskGenerator.name + '\' is not supported.'); } } else if ( algorithm.name === Policy.options.asymmetric.cipher.algorithm.RSA_KEM.name) { if (algorithm.symmetricCipher !== Policy.options.asymmetric.cipher.algorithm.RSA_KEM.symmetricCipher .AES_GCM) { throw new Error( 'Asymmetric encrypter RSA-KEM symmetric cipher algorithm \'' + algorithm.symmetricCipher + '\' is not supported.'); } kemDeriver_ = createDeriver( algorithm.keyDeriver.name, algorithm.keyDeriver.hashAlgorithm); } else { throw new Error( 'Asymmetric encryption algorithm \'' + algorithm.name + '\' is not supported.'); } } // CONSTRUCTOR /////////////////////////////////////////////////////////////// algorithm_ = policy.asymmetric.cipher.algorithm; randomGenerator_ = secureRandomService.newRandomGenerator(); initForgeCipher(algorithm_); // PUBLIC //////////////////////////////////////////////////////////////////// /** * Initializes the asymmetric encrypter with the provided public key. * * @function init * @memberof AsymmetricEncrypter * @param {string} * publicKey The public key with which to initialize the * asymmetric encrypter, in PEM format. * @returns {AsymmetricEncrypter} A reference to this object, to facilitate * method chaining. * @throws {Error} * If the input data validation fails. */ this.init = function(publicKey) { validator.checkIsNonEmptyString( publicKey, 'Public key (PEM encoded) with which to initialize asymmetric encrypter'); forgePublicKey_ = forge.pki.publicKeyFromPem(publicKey); if (typeof forgePublicKey_.encrypt === 'undefined') { throw new Error( 'PEM encoding of public key with which to initialize encrypter is corrupt'); } return this; }; /** * Asymmetrically encrypts the provided data. If the cipher algorithm is set * to RSA-KEM in the cryptographic policy being used, then * the output of this function will be in the form * [Encapsulation][IV][Encrypted Data][Tag]. Before using * this method, the encrypter must have been initialized with a public key, * via the method {@link AsymmetricEncrypter.init}. * * @function encrypt * @memberof AsymmetricEncrypter * @param {Uint8Array|string} * data The data to encrypt. NOTE: Data of type * string will be UTF-8 encoded. * @returns {Uint8Array} The encrypted data. * @throws {Error} * If the input data validation fails, the encrypter was not * initialized or the encryption process fails. */ this.encrypt = function(data) { if (typeof forgePublicKey_ === 'undefined') { throw new Error( 'Asymmetric encrypter has not been initialized with any public key'); } if (typeof data === 'string') { data = codec.utf8Encode(data); } validator.checkIsInstanceOf( data, Uint8Array, 'Uint8Array', 'Data to asymmetrically encrypt'); try { var output; // Choose the encryption algorithm from the policy. switch (algorithm_.name) { case Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name: output = forgePublicKey_.encrypt( codec.binaryEncode(data), algorithm_.name, { md: oaepDigester_, mgf1: {md: oeapMaskDigester_}, seed: codec.binaryEncode(secureRandomService.nextSeed()) }); break; default: output = kemEncrypt( forgePublicKey_, codec.binaryEncode(data), algorithm_, kemDeriver_, randomGenerator_); break; } return codec.binaryDecode(output); } catch (error) { throw new Error('Data could not be encrypted; ' + error); } }; } // Note: at the moment, the options that are being used with the RSA_OAEP // algorithm are hardcoded (for encrypting and decrypting). This could be // improved so that these are read from the properties file. Doing this will // mean any that existing properties files (used by consumers of the // library) will become invalid (if the consumer uses RSA_OAEP) as they wont // have the mandatory new properties. function getRsaOaepHash(hashAlgorithm) { if (hashAlgorithm === Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.hashAlgorithm .SHA256) { return forge.md.sha256.create(); } else { throw new Error( 'RSA-OAEP cipher hash algorithm \'' + hashAlgorithm + '\' is not supported.'); } } function getRsaOaepMaskHash(hashAlgorithm) { // For interoperability purposes, the MGF1 hash function must // remain as SHA-1. if (hashAlgorithm === Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.maskGenerator.MGF1 .hashAlgorithm.SHA1) { return forge.md.sha1.create(); } else { throw new Error( 'RSA-OAEP cipher mask generation function hash algorithm \'' + hashAlgorithm + '\' is not supported.'); } } function createDigester(hashAlgorithm) { if (hashAlgorithm === Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver .hashAlgorithm.SHA256) { return forge.md.sha256.create(); } else if ( hashAlgorithm === Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver .hashAlgorithm.SHA512_224) { return forge.md.sha512.sha224.create(); } else { throw new Error( 'RSA-KEM cipher key derivation hash algorithm \'' + hashAlgorithm + '\' is not supported.'); } } function createDeriver(deriverName, hashAlgorithm) { var hash = createDigester(hashAlgorithm); switch (deriverName) { case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name .KDF1: return new forge.kem.kdf1(hash); case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name .KDF2: return new forge.kem.kdf2(hash); case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name .MGF1: return new forge.mgf.mgf1.create(hash); default: throw new Error( 'RSA-KEM cipher key derivation function \'' + deriverName + '\' is not supported.'); } } /** * Encrypts some data using the 'RSA-KEM' cipher algorithm. * * @function kemEncrypt * @memberof AsymmetricEncrypter * @private * @param {Object} * publicKey The public key to use for encrypting * @param {Object} * data The data to encrypt. * @param {Object} * algorithm The cipher algorithm. * @param {Object} * randomGenerator The random generator to use. * @returns {Object} The encrypted data. */ function kemEncrypt(publicKey, data, algorithm, deriver, randomGenerator) { // generate and encapsulate secret key var kem = forge.kem.rsa.create(deriver); var result = kem.encrypt(publicKey, algorithm.secretKeyLengthBytes); var iv = codec.binaryEncode(randomGenerator.nextBytes(algorithm.ivLengthBytes)); var cipher = forge.cipher.createCipher(algorithm.symmetricCipher, result.key); cipher.start({iv: iv}); cipher.update(forge.util.createBuffer(data)); cipher.finish(); var encryptedData = cipher.output.getBytes(); var tag = cipher.mode.tag.getBytes(); return result.encapsulation.toString() + iv.toString() + encryptedData.toString() + tag.toString(); }