/*******************************************************************************
 *  Copyright 2017-2018 Centre for Development of Advanced Computing(C-DAC), Pune
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *   http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package cdac.medinfo.sdk.dcm30_04.services.dsp;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


import cdac.medinfo.sdk.dcm30_04.baselibs.DicomV3Factory;
import cdac.medinfo.sdk.dcm30_04.baselibs.DigitalSignaturesSequence;
import cdac.medinfo.sdk.dcm30_04.baselibs.DigitalSignaturesSequenceItem;
import cdac.medinfo.sdk.dcm30_04.baselibs.ExplicitDE;
import cdac.medinfo.sdk.dcm30_04.baselibs.ImplicitDE;
import cdac.medinfo.sdk.dcm30_04.baselibs.MACParametersSequence;
import cdac.medinfo.sdk.dcm30_04.baselibs.MACParametersSequenceItem;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataElement;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataSet;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataTypeSQ;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IItem;
import cdac.medinfo.sdk.dcm30_04.commoninterface.ITag;
import cdac.medinfo.sdk.dcm30_04.exceptions.DSPException;
import cdac.medinfo.sdk.dcm30_04.utils.DicomConfig;
import cdac.medinfo.sdk.dcm30_04.utils.DicomConstants;
import cdac.medinfo.sdk.dcm30_04.utils.EnumFeatureList;
import cdac.medinfo.sdk.dcm30_04.utils.ICollectionIterator;
import cdac.medinfo.sdk.dcm30_04.utils.ISingleCollection;
import cdac.medinfo.sdk.dcm30_04.utils.ResourceBundleWrapper;
import cdac.medinfo.sdk.dcm30_04.utils.SingleCollection;


/**
 * Creates a Digital Signature in the Data Set for specified Digital Signature Profile and
 * also used to retrieving Digital Signature UIDs from DataSet. This class signs DataSet with
 * algorithm SHA1withRSA and accepts X.509 Certificate.
 */
public class DSPSigner
{
	private DSPUtils objUtil = null;
	private final String SIGN_ALGORITHM = "SHA1withRSA";
	private final String CERTIFICATE_TYPE = "X.509";
	private final String DATE_TIME_FORMAT = "yyyyMMddhhmmss";

	
	
	/**
	 * Initializes a new instance of the DSPSigner class.  
	 */
	public DSPSigner() 
	{
		objUtil = new DSPUtils();
	}

	/**
	 * Adds new Digital Signature in specified DataSet. 
	 * @param objIDataSet specified instance of DataSet in which Digital Signature will be added.
	 * @param objRSAPrivateKey Private key which will be used to sign the Message Authentication Code (MAC).
	 * @param objCerificate Digital certificate of the signer, This will be the Certificate of Signer (0400,0115). 
	 * @param objDEtoSign  SingleCollection contains Tags of the Data Elements to be signed,
	 *        if null then all/minimum set of Data Elements of the Data Set may be referenced by the Tags depending on Digital 
	 *        SignatureProfile. Also in case of no Data Element is present in dataset from specified Collection of Tags then all
	 *        the Data Elements are referenced. 
	 * @param enumDigitalSignatureProfile RSA Digital Signature Profile.
	 * @param enumDicomMACAlgorithm algorithm used in generating the MAC to be signed to form the Digital Signature.
	 * @param shMACIDNumber A number used to identify which MAC Parameters Sequence item was used in the calculation of this Digital Signature.
	 * @param strDigitalSignatureUID A UID that can be used to uniquely reference this signature.
	 * @param strCertifiedTimestampType Certified Timestamp Type required in case of implementation of Public Key Infrastructure Time Stamp Protocol.
	 * @param vectCertifiedTimestamp A certified timestamp of the Digital Signature required in case of implementation of Public Key Infrastructure Time Stamp Protocol.
	 * @throws IOException  Signals that an I/O exception of some sort has occurred.
	 * @throws NoSuchAlgorithmException This exception is thrown when a particular cryptographic algorithm is requested but is not available in the environment.
	 * @throws NoSuchProviderException This exception is thrown when a particular security provider is requested but is not available in the environment.
	 * @throws InvalidKeySpecException This is the exception for invalid key specifications.
	 * @throws InvalidKeyException exception for invalid Keys (invalid encoding, wrong length, uninitialized, etc).
	 * @throws SignatureException generic Signature exception. 
	 * @throws CertificateEncodingException Certificate Encoding Exception. This is thrown whenever an error occurs while attempting to encode a certificate.
	 */
	public void addDigitalSignature(
			IDataSet objIDataSet,			
			RSAPrivateKey objRSAPrivateKey,
			Certificate objCerificate,
			ISingleCollection<ITag> objDEtoSign,
			EnumDigitalSignatureProfile enumDigitalSignatureProfile,
			EnumMACAlgorithm enumDicomMACAlgorithm,
			short shMACIDNumber,
			String strDigitalSignatureUID,
			String strCertifiedTimestampType,
			ISingleCollection<byte[]> vectCertifiedTimestamp) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, InvalidKeyException, SignatureException, CertificateEncodingException
			{

		MACParametersSequence objMACParametersSequence = (MACParametersSequence)DicomV3Factory.createInstance().createSequence(0x4FFE,0x0001, objIDataSet.getTransferSyntax());
		MACParametersSequenceItem  objMACParametersSequenceItem = objMACParametersSequence.getNewItem();

		DigitalSignaturesSequence objDigitalSignaturesSequence = (DigitalSignaturesSequence) DicomV3Factory.createInstance().createSequence(0xFFFA,0xFFFA, objIDataSet.getTransferSyntax());
		DigitalSignaturesSequenceItem objDigitalSignaturesSequenceItem = objDigitalSignaturesSequence.getNewItem();

		// Duplicate MAC ID check
		ISingleCollection<Short> objMacIDCollection = getMACIDs(objIDataSet);
		if(null != objMacIDCollection && objMacIDCollection.contains(shMACIDNumber))
			throw new DSPException(ResourceBundleWrapper.getValue("exceptions", "DSP.DuplicateMACID", new Locale(DicomConfig.createInstance().getLocale())));
		

		// Duplicate Digital Signature UID check
		ISingleCollection<String> objDSUIDCollection = getDigitalSignatureUIDs(objIDataSet);
		if(null != objDSUIDCollection && objDSUIDCollection.contains(strDigitalSignatureUID))
			throw new DSPException(ResourceBundleWrapper.getValue("exceptions", "DSP.DuplicateDigitalSignatureUID", new Locale(DicomConfig.createInstance().getLocale())));
		
		//Prepare Message Authentication code parameter Sequence Item.
		objMACParametersSequenceItem.setMACIDNumber(shMACIDNumber);
		objMACParametersSequenceItem.setMACCalculationTransferSyntaxUID(objIDataSet.getTransferSyntax().getTransferSyntaxUID());
		objMACParametersSequenceItem.setMACAlgorithm(enumDicomMACAlgorithm.getValue());
		//objMACParametersSequenceItem.setDataElementsSigned(objDEtoSign);

		//Prepare Digital Signature Sequence Item.
		objDigitalSignaturesSequenceItem.setMACIDNumber(shMACIDNumber);
		objDigitalSignaturesSequenceItem.setDigitalSignatureUID(strDigitalSignatureUID);

		SimpleDateFormat parse = new SimpleDateFormat(DATE_TIME_FORMAT);
		String strDateTime = parse.format(new Date());
		objDigitalSignaturesSequenceItem.setDigitalSignatureDateTime(strDateTime);		
		objDigitalSignaturesSequenceItem.setCertificateType(objCerificate.getType());

		ISingleCollection<byte[]> objCertificateofSigner  = new SingleCollection<byte[]>();
		objCertificateofSigner.add(objCerificate.getEncoded());

		objDigitalSignaturesSequenceItem.setCertificateOfSigner(objCertificateofSigner);

		Signature objRSASignature = Signature.getInstance(SIGN_ALGORITHM);
		objRSASignature.initSign(objRSAPrivateKey);
		ISingleCollection<byte[]> objSignature = new SingleCollection<byte[]>();
		ISingleCollection<ITag> objDESigned = null;
		byte[] data = null,mac;
		byte[] Signature;
		Mac objMac = Mac.getInstance(enumDicomMACAlgorithm.getValue());		

		// Encoded Certificate will be used as Secret Key
		SecretKeySpec objKeySpec = new SecretKeySpec(objCerificate.getEncoded(),enumDicomMACAlgorithm.getValue());
		objMac.init(objKeySpec);

		switch (enumDigitalSignatureProfile) 
		{
		case BASERSA:
			// Consider minimum set
			if(objDEtoSign == null)	
			{
				data = objUtil.getDataSetBytesForBaseRSA(objIDataSet);				
			}
			else
			{
				objDESigned = objDEtoSign;
				data = objUtil.getDataSetBytesForTags(objIDataSet, objDESigned);
			}
			break;
		case CREATORRSA:
			objDESigned = objUtil.getTagCollectionForCreatorRSA(); // Consider minimum set			
			if(objDEtoSign != null)
				for (ITag tag : objDEtoSign) 
				{
					if(!objDESigned.contains(tag))
						objDESigned.add(tag);
				}		
			data = objUtil.getDataSetBytesForTags(objIDataSet, objDESigned);
			break;
		case AUTHORIZATIONRSA:
			objDESigned = objUtil.getTagCollectionForAuthorizationRSA(); // Consider minimum set			
			if(objDEtoSign != null)
				for (ITag tag : objDEtoSign) 
				{
					if(!objDESigned.contains(tag))
						objDESigned.add(tag);
				}		

			data = objUtil.getDataSetBytesForTags(objIDataSet, objDESigned);	
			break;		
		}

		objMac.update(data);
		mac = objMac.doFinal(); // Generate MAC 				
		objRSASignature.update(mac); 
		Signature = objRSASignature.sign(); // Sign MAC
		objSignature.add(Signature);

		// Set Signature.
		objDigitalSignaturesSequenceItem.setSignature(objSignature);
		// Set DE  Signed
		objMACParametersSequenceItem.setDataElementsSigned(objDESigned);
		objDigitalSignaturesSequenceItem.setCertifiedTimestampType(strCertifiedTimestampType);
		objDigitalSignaturesSequenceItem.setCertifiedTimestamp(vectCertifiedTimestamp);


		// Add Items to Sequence
		objMACParametersSequence.addItem(objMACParametersSequenceItem);
		objDigitalSignaturesSequence.addItem(objDigitalSignaturesSequenceItem);


		//Add MAC Parameter Sequence to DataSet
		IDataElement objIDataElement = null;
		if(objIDataSet.getTransferSyntax().getVRTransferSyntax()==DicomConstants.DCM_VR_IMPLICIT)
			objIDataElement = new ImplicitDE(0x4FFE,0x0001,objMACParametersSequence,objMACParametersSequence.getTransferSyntax(),objIDataSet.getDictionaryReader());
		else
			objIDataElement = new ExplicitDE(0x4FFE,0x0001,DicomConstants.DCM_DT_SQ,objMACParametersSequence,objMACParametersSequence.getTransferSyntax(),objIDataSet.getDictionaryReader());

		if(!objIDataSet.isPresent(0x4FFE,0x0001))
			objIDataSet.addDataElement(objIDataElement);
		else
			((MACParametersSequence)objIDataSet.getDataForTag(0x4FFE,0x0001).getDicomValue()).addItem(objMACParametersSequenceItem);

		// Add Digital Signature Sequence to DataSet
		if(objIDataSet.getTransferSyntax().getVRTransferSyntax()==DicomConstants.DCM_VR_IMPLICIT)
			objIDataElement = new ImplicitDE(0xFFFA,0xFFFA,objDigitalSignaturesSequence,objDigitalSignaturesSequence.getTransferSyntax(),objIDataSet.getDictionaryReader());
		else
			objIDataElement = new ExplicitDE(0xFFFA,0xFFFA,DicomConstants.DCM_DT_SQ,objDigitalSignaturesSequence,objDigitalSignaturesSequence.getTransferSyntax(),objIDataSet.getDictionaryReader());

		if(!objIDataSet.isPresent(0xFFFA,0xFFFA))
			objIDataSet.addDataElement(objIDataElement);
		else
			((DigitalSignaturesSequence)objIDataSet.getDataForTag(0xFFFA,0xFFFA).getDicomValue()).addItem(objDigitalSignaturesSequenceItem);		

			}


	//The name of the file that stores the digital certificate of the signer. This will be the Certificate of Signer (0400,0115). The method accepts X.509 digital certificates 


	/**
	 * Adds new Digital Signature in specified DataSet. 
	 * @param objIDataSet specified instance of DataSet in which Digital Signature will be added.
	 * @param objRSAPrivateKey Private key which will be used to sign the Message Authentication Code (MAC).
	 * @param certificateFile The name of the file that stores the digital certificate of the signer. This will be the Certificate of Signer (0400,0115).  
	 * @param objDEtoSign  SingleCollection contains Tags of the Data Elements to be signed,
	 *        if null then all/minimum set of Data Elements of the Data Set may be referenced by the Tags depending on Digital 
	 *        SignatureProfile. Also in case of no Data Element is present in dataset from specified Collection of Tags then all
	 *        the Data Elements are referenced. 
	 * @param enumDigitalSignatureProfile RSA Digital Signature Profile.
	 * @param enumDicomMACAlgorithm algorithm used in generating the MAC to be signed to form the Digital Signature.
	 * @param shMACIDNumber A number used to identify which MAC Parameters Sequence item was used in the calculation of this Digital Signature.
	 * @param strDigitalSignatureUID A UID that can be used to uniquely reference this signature.
	 * @param strCertifiedTimestampType Certified Timestamp Type required in case of implementation of Public Key Infrastructure Time Stamp Protocol.
	 * @param vectCertifiedTimestamp A certified timestamp of the Digital Signature required in case of implementation of Public Key Infrastructure Time Stamp Protocol.
	 * @throws IOException  Signals that an I/O exception of some sort has occurred.
	 * @throws NoSuchAlgorithmException This exception is thrown when a particular cryptographic algorithm is requested but is not available in the environment.
	 * @throws NoSuchProviderException This exception is thrown when a particular security provider is requested but is not available in the environment.
	 * @throws InvalidKeySpecException This is the exception for invalid key specifications.
	 * @throws InvalidKeyException exception for invalid Keys (invalid encoding, wrong length, uninitialized, etc).
	 * @throws SignatureException generic Signature exception. 
	 * @throws CertificateEncodingException Certificate Encoding Exception. This is thrown whenever an error occurs while attempting to encode a certificate.
	 */
	public void addDigitalSignature(
			IDataSet objIDataSet,			
			RSAPrivateKey objRSAPrivateKey,
			String certificateFile,
			ISingleCollection<ITag> objDEtoSign,
			EnumDigitalSignatureProfile enumDigitalSignatureProfile,
			EnumMACAlgorithm enumDicomMACAlgorithm,
			short shMACIDNumber,
			String strDigitalSignatureUID,
			String strCertifiedTimestampType,
			ISingleCollection<byte[]> vectCertifiedTimestamp) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, InvalidKeyException, SignatureException, CertificateException
			{
		CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
		FileInputStream fis = new FileInputStream(certificateFile);
		BufferedInputStream bis = new BufferedInputStream(fis);
		X509Certificate objX509Certificate = (X509Certificate)cf.generateCertificate(bis);
		bis.close();
		addDigitalSignature(
				objIDataSet,objRSAPrivateKey,objX509Certificate,
				objDEtoSign,enumDigitalSignatureProfile,enumDicomMACAlgorithm,
				shMACIDNumber,strDigitalSignatureUID,strCertifiedTimestampType,
				vectCertifiedTimestamp);
			}


	/**
	 * Retrieves the Collection of Digital Signature UIDs from specified DataSet.
	 * @param objDataSet specified instance of DataSet
	 * @return SignleCollection UIDs that can be used to uniquely reference the Digital Signature.
	 */
	public ISingleCollection<String> getDigitalSignatureUIDs(IDataSet objDataSet)
	{		
		ISingleCollection<String> objSingleCollection = new SingleCollection<String>();
		if(objDataSet.isPresent(0xFFFA,0xFFFA))
		{
			IDataTypeSQ objDigitalSignaturesSequence = (DigitalSignaturesSequence)objDataSet.getDataForTag(0xFFFA,0xFFFA).getDicomValue();
			ICollectionIterator<IItem> objDigitalSignaturesSequenceItemIterator = objDigitalSignaturesSequence.iterator();
			while(objDigitalSignaturesSequenceItemIterator.hasNext())
			{
				DigitalSignaturesSequenceItem objDigitalSignaturesSequenceItem = (DigitalSignaturesSequenceItem)objDigitalSignaturesSequenceItemIterator.next();
				objSingleCollection.add(objDigitalSignaturesSequenceItem.getDigitalSignatureUID());
			}
			return objSingleCollection;
		}
		return null;
	}

	
//	shMACIDNumber A number used to identify which MAC Parameters Sequence item was used in the calculation of this Digital Signature.
	
	/**
	 * Retrieves the Collection of numbers used to identify which MAC Parameters Sequence item was used in the calculation of this Digital Signature.
	 * @param objDataSet specified instance of DataSet
	 * @return SignleCollection numbers used to identify which MAC Parameters Sequence items.
	 */
	public ISingleCollection<Short> getMACIDs(IDataSet objDataSet)
	{		
		ISingleCollection<Short> objSingleCollection = new SingleCollection<Short>();
		if(objDataSet.isPresent(0x4FFE,0x0001)) // check presence of MAC parameter sequence.
		{
			IDataTypeSQ objMACParametersSequence = (MACParametersSequence)objDataSet.getDataForTag(0x4FFE,0x0001).getDicomValue();
			ICollectionIterator<IItem> objMACParametersSequenceItemIterator = objMACParametersSequence.iterator();
			while(objMACParametersSequenceItemIterator.hasNext())
			{
				MACParametersSequenceItem objDigitalSignaturesSequenceItem = (MACParametersSequenceItem)objMACParametersSequenceItemIterator.next();
				objSingleCollection.add(objDigitalSignaturesSequenceItem.getMACIDNumber());
			}
			return objSingleCollection;
		}
		return null;
	}
}
