/*
* 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 bitwise = require('scytl-bitwise');
var codec = require('scytl-codec');
var forge = require('node-forge');
module.exports = AsymmetricDecrypter;
/**
* @class AsymmetricDecrypter
* @classdesc The asymmetric decrypter API. To instantiate this object, use the
* method {@link AsymmetricCryptographyService.newDecrypter}.
* @hideconstructor
* @param {Policy}
* policy The cryptographic policy to use.
*/
function AsymmetricDecrypter(policy) {
// PRIVATE ///////////////////////////////////////////////////////////////////
var algorithm_;
var oaepDigester_;
var oeapMaskDigester_;
var kemDeriver_;
var forgePrivateKey_;
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(
'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(
'RSA-KEM symmetric cipher algorithm \'' +
algorithm.symmetricCipher + '\' is not supported.');
}
kemDeriver_ = createDeriver(
algorithm.keyDeriver.name, algorithm.keyDeriver.hashAlgorithm);
} else {
throw new Error(
'Asymmetric decryption algorithm \'' + algorithm.name +
'\' is not supported.');
}
}
// CONSTRUCTOR ///////////////////////////////////////////////////////////////
algorithm_ = policy.asymmetric.cipher.algorithm;
initForgeCipher(algorithm_);
// PUBLIC ////////////////////////////////////////////////////////////////////
/**
* Initializes the asymmetric decrypter with the provided private key.
*
* @function init
* @memberof AsymmetricDecrypter
* @param {string}
* privateKey The private key with which to initialize the
* asymmetric decrypter, in PEM format.
* @returns {AsymmetricDecrypter} A reference to this object, to facilitate
* method chaining.
* @throws {Error}
* If the input data validation fails.
*/
this.init = function(privateKey) {
validator.checkIsNonEmptyString(
privateKey,
'Private key (PEM encoded) with which to initialize asymmetric decrypter');
forgePrivateKey_ = forge.pki.privateKeyFromPem(privateKey);
if (typeof forgePrivateKey_.decrypt === 'undefined') {
throw new Error(
'PEM encoding of private key with which to initialize asymmetric decrypter is corrupt');
}
return this;
};
/**
* Asymmetrically decrypts the provided data. Before using this method, the
* decrypter must have been initialized with a private key, via the method
* {@link AsymmetricDecrypter.init}.
*
* @function decrypt
* @memberof AsymmetricDecrypter
* @param {Uint8Array}
* encryptedData The encrypted data to decrypt.
* @returns {Uint8Array} The decrypted data. NOTE: To retrieve data
* of type string
, apply method
* codec.utf8Decode
to result.
* @throws {Error}
* If the input data validation fails, the decrypter was not
* initialized or the decryption process fails.
*/
this.decrypt = function(encryptedData) {
if (typeof forgePrivateKey_ === 'undefined') {
throw new Error(
'Asymmetric decrypter has not been initialized with any private key');
}
validator.checkIsInstanceOf(
encryptedData, Uint8Array, 'Uint8Array',
'Encrypted data to asymmetrically decrypt');
try {
var decryptedData;
// Choose a decryption algorithm from the policy.
switch (algorithm_.name) {
case Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name:
decryptedData = forgePrivateKey_.decrypt(
encryptedData, algorithm_.name,
{md: oaepDigester_, mgf1: {md: oeapMaskDigester_}});
break;
default:
decryptedData = kemDecrypt(
forgePrivateKey_, encryptedData, algorithm_, kemDeriver_);
}
return codec.binaryDecode(decryptedData);
} catch (error) {
throw new Error('Data could not be decrypted; ' + 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.');
}
}
/**
* Decrypt some encrypted data using the 'RSA-KEM' cipher algorithm.
*
* @function kemDecrypt
* @memberof AsymmetricDecrypter
* @private
* @param {Object}
* privateKey The private key used for decrypting.
* @param {Object}
* encryptedData The encrypted data to decrypt.
* @param {Object}
* algorithm The cipher algorithm.
* @returns {Object} The decrypted data.
*/
function kemDecrypt(privateKey, encryptedData, algorithm, deriver) {
var encryptedDataParts = parseParts(privateKey, encryptedData, algorithm);
// var deriver = createDeriver(
// algorithm.keyDeriver.name, algorithm.keyDeriver.hashAlgorithm);
// decrypt encapsulated secret key
var kem = forge.kem.rsa.create(deriver);
var key = kem.decrypt(
privateKey, encryptedDataParts.encapsulation,
algorithm.secretKeyLengthBytes);
// decrypt some bytes
var decipher = forge.cipher.createDecipher(algorithm.symmetricCipher, key);
decipher.start({iv: encryptedDataParts.iv, tag: encryptedDataParts.tag});
decipher.update(forge.util.createBuffer(encryptedDataParts.data));
var result = decipher.finish();
// result will be false if there was a failure
if (result) {
return decipher.output.getBytes();
} else {
throw new Error('Data could not be KEM decrypted.');
}
}
/**
* Parses the four parts of the data that are produced by the encrypt function,
* and that are received by the decrypt function.
*
* We know the length of three of these parts, and we know the total length of * the data, therefore we can parse all four of the parts. * * @function parseParts * @memberof AsymmetricDecrypter * @private * @param {Object} * privateKey The private key used for decrypting. * @param {Object} * encryptedData The encrypted data to decrypt. * @param {Object} * algorithm The cipher algorithm. returns {Object} The object * encapsulating the parsing results. */ function parseParts(privateKey, encryptedData, algorithm) { var ivLengthBytes = algorithm.ivLengthBytes; var tagLengthBytes = algorithm.tagLengthBytes; var encapsulationLengthBytes = privateKey.n.bitLength() / 8; var totalLength = encryptedData.length; var totalKnownLength = encapsulationLengthBytes + ivLengthBytes + tagLengthBytes; var encryptedDataLength = totalLength - totalKnownLength; var startIndexOfSecondPart = encapsulationLengthBytes; var startIndexOfThirdPart = startIndexOfSecondPart + ivLengthBytes; var startIndexOfFourthPart = startIndexOfThirdPart + encryptedDataLength; var encapsulation = bitwise.slice(encryptedData, 0, startIndexOfSecondPart); var iv = bitwise.slice( encryptedData, startIndexOfSecondPart, startIndexOfThirdPart); var data = bitwise.slice( encryptedData, startIndexOfThirdPart, startIndexOfFourthPart); var tag = bitwise.slice(encryptedData, startIndexOfFourthPart); return { encapsulation: codec.binaryEncode(encapsulation), iv: codec.binaryEncode(iv), data: codec.binaryEncode(data), tag: codec.binaryEncode(tag) }; }