/*
* 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;
};
}