/* * 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 = Signer; /** * @class Signer * @classdesc The digital signer API. To instantiate this object, use the method * {@link AsymmetricCryptographyService.newSigner}. * @hideconstructor * @param {Policy} * policy The cryptographic policy to use. * @param {SecureRandomService} * secureRandomService The secure random service to use. */ function Signer(policy, secureRandomService) { // PRIVATE /////////////////////////////////////////////////////////////////// var digester_; var padding_; var forgePrivateKey_; var updated_; function initForgeSigner() { if (policy.asymmetric.signer.algorithm !== Policy.options.asymmetric.signer.algorithm.RSA) { throw new Error( 'Signer algorithm \'' + policy.asymmetric.signer.algorithm + '\' is not supported.'); } if (policy.asymmetric.signer.hashAlgorithm === Policy.options.asymmetric.signer.hashAlgorithm.SHA256) { digester_ = forge.md.sha256.create(); } else if ( policy.asymmetric.signer.hashAlgorithm === Policy.options.asymmetric.signer.hashAlgorithm.SHA512_224) { digester_ = forge.md.sha512.sha224.create(); } else { throw new Error( 'Signer hash algorithm \'' + policy.asymmetric.signer.hashAlgorithm + '\' is not supported.'); } digester_.start(); if (typeof policy.asymmetric.signer.padding !== 'undefined') { if (policy.asymmetric.signer.padding.name === Policy.options.asymmetric.signer.padding.PSS.name) { var paddingMd; if (policy.asymmetric.signer.padding.hashAlgorithm === Policy.options.asymmetric.signer.padding.PSS.hashAlgorithm.SHA256) { paddingMd = forge.md.sha256.create(); } else if ( policy.asymmetric.signer.padding.hashAlgorithm === Policy.options.asymmetric.signer.padding.PSS.hashAlgorithm .SHA512_224) { paddingMd = forge.md.sha512.sha224.create(); } else { throw new Error( 'Signer PSS padding hash algorithm \'' + policy.asymmetric.signer.padding.hashAlgorithm + '\' is not supported.'); } var paddingMgf; if (policy.asymmetric.signer.padding.maskGenerator.name === Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1 .name) { if (policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm === Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1 .hashAlgorithm.SHA256) { paddingMgf = forge.mgf.mgf1.create(forge.md.sha256.create()); } else if ( policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm === Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1 .hashAlgorithm.SHA512_224) { paddingMgf = forge.mgf.mgf1.create(forge.md.sha512.sha224.create()); } else { throw new Error( 'Signer PSS padding mask generation function hash algorithm \'' + policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm + '\' is not supported.'); } } else { throw new Error( 'Signer PSS padding mask generation function \'' + policy.asymmetric.signer.padding.maskGenerator.name + '\' is not supported.'); } padding_ = forge.pss.create({ md: paddingMd, mgf: paddingMgf, saltLength: policy.asymmetric.signer.padding.saltLengthBytes, prng: secureRandomService.newRandomGenerator() }); } else { throw new Error( 'Signer padding \'' + policy.asymmetric.signer.padding.name + '\' is not supported.'); } } updated_ = false; } // CONSTRUCTOR /////////////////////////////////////////////////////////////// initForgeSigner(); // PUBLIC //////////////////////////////////////////////////////////////////// /** * Initializes the digital signer with the provided private key. * * @function init * @memberof Signer * @param {string} * privateKeyPem The private key with which to initialize the * digital signer, in PEM format. * @returns {Signer} A reference to this object, to facilitate method * chaining. * @throws {Error} * If the input data validation fails. */ this.init = function(privateKeyPem) { validator.checkIsNonEmptyString( privateKeyPem, 'Private key (PEM encoded) with which to initialize digital signer'); forgePrivateKey_ = forge.pki.privateKeyFromPem(privateKeyPem); if (typeof forgePrivateKey_.sign === 'undefined') { throw new Error( 'PEM encoding of private key with which to initialize digital signer is corrupt'); } return this; }; /** * Digitally signs the provided data. If there were any prior calls to the * method update, then the provided data will be bitwise * appended to the data provided to those calls. If no data is provided here * the signature will only be generated for the data provided to prior calls * to the method update. The signer will be automatically * reinitialized after this method completes. Before * using this method, the signer must have been initialized with a private * key, via the method {@link Signer.init}. * * @function sign * @memberof Signer * @param {Uint8Array|string} * [data] Some data to sign. NOTE: Data of type * string will be UTF-8 encoded. * @returns {Uint8Array} The digital signature. * @throws {Error} * If the input data validation fails, the signer was not * initialized, the signer was not updated with any data or the * digital signing process fails. */ this.sign = function(data) { if (typeof forgePrivateKey_ === 'undefined') { throw new Error( 'Could not sign; Digital signer has not been initialized with any private key'); } if (typeof data !== 'undefined') { if (typeof data === 'string') { data = codec.utf8Encode(data); } validator.checkIsInstanceOf( data, Uint8Array, 'Uint8Array', 'Data provided to digital signer'); this.update(data); } else if (!updated_) { throw new Error( 'Attempt to digitally sign without either providing data as input or having made a previous call to method \'update\''); } var publicExponentFound = forgePrivateKey_.e.toString(); var publicExponentExpected = policy.asymmetric.signer.publicExponent.toString(); if (publicExponentFound !== publicExponentExpected) { throw new Error( 'Expected private key for signing data to have same public exponent as crytographic policy: ' + publicExponentExpected + ' ; Found ' + publicExponentFound); } try { var signature = forgePrivateKey_.sign(digester_, padding_); digester_.start(); updated_ = false; return codec.binaryDecode(signature); } catch (error) { throw new Error('Signature could not be generated; ' + error); } }; /** * Updates the signer with the provided data. The data will be internally * bitwise concatenated to any data provided during previous calls to this * method, after the last call to the method sign. Before * using this method, the signer must have been initialized with a private * key, via the method {@link Signer.init}. * * @function update * @memberof Signer * @param {Uint8Array|string} * data The data with which to update the signer. NOTE: * Data of type string will be UTF-8 encoded. * @returns {Signer} A reference to this object, to facilitate method * chaining. * @throws {Error} * If the input data validation fails or the update process * fails. */ this.update = function(data) { if (typeof forgePrivateKey_ === 'undefined') { throw new Error( 'Could not update; Digital signer has not been initialized with any private key'); } if (typeof data === 'string') { data = codec.utf8Encode(data); } validator.checkIsInstanceOf( data, Uint8Array, 'Uint8Array', 'Data with which to update digital signer'); try { digester_.update(codec.binaryEncode(data)); updated_ = true; } catch (error) { throw new Error('Signer could not be updated: ' + error.message); } return this; }; }