/*
 * 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
 */
package com.scytl.products.ov.config.shares.service;

import static org.junit.Assert.assertEquals;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalDecrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.config.multishare.ElGamalPrivateKeySharesService;
import com.scytl.products.ov.config.multishare.MultipleSharesContainer;
import com.scytl.products.ov.config.shares.exception.SharesException;
import com.scytl.products.ov.config.shares.exception.SmartcardException;
import com.scytl.products.ov.config.shares.handler.StatelessReadSharesHandler;
import com.scytl.products.ov.config.shares.keys.elgamal.ElGamalKeyPairGenerator;
import com.scytl.products.ov.config.shares.keys.elgamal.ElGamalPrivateKeyAdapter;
import com.scytl.products.ov.config.shares.keys.elgamal.ElGamalPublicKeyAdapter;
import com.scytl.products.ov.config.shares.keys.rsa.RSAKeyPairGenerator;
import com.scytl.products.ov.config.shares.keys.rsa.RSAPrivateKeySerializer;
import com.scytl.shares.Share;
import com.scytl.shares.ShareException;

@Ignore
public class DefaultSmartCardServiceTest {

    private static DefaultSmartCardService _target = new DefaultSmartCardService();

    private static StatelessReadSharesHandler _statelessReadSharesHandler;

    private static AsymmetricServiceAPI _asymmetricServiceAPI;

    private static ElGamalServiceAPI _elGamalServiceAPI;

    private static SmartCardService _smartcardService;

    private static RSAKeyPairGenerator _rsaKeyPairGenerator;

    private static PrivateKeySharesService _splitService;

    private static ZpSubgroup _group;

    private static ElGamalEncryptionParameters _elGamalEncryptionParameters;

    private static final String OLD_PIN_PUK = "222222";

    private static final String NEW_PIN_PUK = "222222";

    @BeforeClass
    public static void init() throws GeneralCryptoLibException, SharesException {

        _asymmetricServiceAPI = new AsymmetricService();

        _elGamalServiceAPI = new ElGamalService();

        _smartcardService = SmartCardServiceFactory.getSmartCardService(true);

        _rsaKeyPairGenerator = new RSAKeyPairGenerator(_asymmetricServiceAPI);

        RSAPrivateKeySerializer rsaPrivateKeySerializer = new RSAPrivateKeySerializer();

        _splitService = new PrivateKeySharesService(rsaPrivateKeySerializer);

        _statelessReadSharesHandler = new StatelessReadSharesHandler(rsaPrivateKeySerializer, _smartcardService,
            _asymmetricServiceAPI, _elGamalServiceAPI);

        BigInteger _p = new BigInteger(
            "16370518994319586760319791526293535327576438646782139419846004180837103527129035954742043590609421369665944746587885814920851694546456891767644945459124422553763416586515339978014154452159687109161090635367600349264934924141746082060353483306855352192358732451955232000593777554431798981574529854314651092086488426390776811367125009551346089319315111509277347117467107914073639456805159094562593954195960531136052208019343392906816001017488051366518122404819967204601427304267380238263913892658950281593755894747339126531018026798982785331079065126375455293409065540731646939808640273393855256230820509217411510058759");
        BigInteger _q = new BigInteger(
            "8185259497159793380159895763146767663788219323391069709923002090418551763564517977371021795304710684832972373293942907460425847273228445883822472729562211276881708293257669989007077226079843554580545317683800174632467462070873041030176741653427676096179366225977616000296888777215899490787264927157325546043244213195388405683562504775673044659657555754638673558733553957036819728402579547281296977097980265568026104009671696453408000508744025683259061202409983602300713652133690119131956946329475140796877947373669563265509013399491392665539532563187727646704532770365823469904320136696927628115410254608705755029379");
        BigInteger _g = new BigInteger("2");

        _group = new ZpSubgroup(_g, _p, _q);

        _elGamalEncryptionParameters = new ElGamalEncryptionParameters(_p, _q, _g);
    }

    @Test
    public void whenWriteAndReadRsaPrivateKeyAsSharesThenOk()
            throws SharesException, KeyException, SmartcardException, IOException, GeneralCryptoLibException {

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        int numShares = 2;
        int threshold = 2;
        String name = "name";

        System.out.println("Will generate a keypair. Press Enter to continue");
        in.readLine();

        KeyPair keyPair;
        keyPair = _rsaKeyPairGenerator.generate();

        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();

        String _secretDataAsString = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
        byte[] ciphertext = _asymmetricServiceAPI.encrypt(publicKey, _secretDataAsString.getBytes());

        System.out.println("Will split privatekey into shares. Press Enter to continue");
        in.readLine();

        List<Share> share = _splitService.split(privateKey, numShares, threshold);
        System.out.println("Number of shares: " + share.size());

        System.out
            .println("Starting the process of writing to smartcards. Will write to " + numShares + " smartcards.");

        for (int i = 0; i < numShares; i++) {

            int indexToDisplay = i + 1;
            System.out.println("Please insert smartcard " + indexToDisplay + " and press Enter");
            in.readLine();

            String nameToWrite = name + "_" + i;
            _target.write(share.get(i), nameToWrite, OLD_PIN_PUK, NEW_PIN_PUK, privateKey);
        }

        System.out.println("Will begin to read data back from smartcards. Press Enter");
        in.readLine();

        Set<Share> readShares = new HashSet<Share>();
        for (int i = 0; i < threshold; i++) {

            int indexToDisplay = i + 1;
            System.out.println("Please insert smartcard " + indexToDisplay + " and press Enter");
            in.readLine();

            readShares.add(_target.read(NEW_PIN_PUK, publicKey));
        }

        PrivateKey reconstructedPrivateKey = _statelessReadSharesHandler.getPrivateKey(readShares, publicKey);

        byte[] plaintext = _asymmetricServiceAPI.decrypt(reconstructedPrivateKey, ciphertext);
        String recoveredString = new String(plaintext);

        String errorMsg = "The recovered plaintext does not match the original plaintext";
        assertEquals(errorMsg, _secretDataAsString, recoveredString);

        System.out
            .println("Successfully reconstructed the key. The algorithm is: " + reconstructedPrivateKey.getAlgorithm());
    }

    @Test
    public void whenWriteAndReadElGamalPrivateKeyAsSharesThenOk()
            throws SharesException, KeyException, SmartcardException, IOException, GeneralCryptoLibException,
            ShareException {

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        int numShares = 2;
        int threshold = 2;
        String name = "name";

        ElGamalKeyPairGenerator elGamalKeyPairGenerator =
            new ElGamalKeyPairGenerator(_elGamalEncryptionParameters, 10, _elGamalServiceAPI);

        KeyPair signAuthorityKeyPair = _asymmetricServiceAPI.getKeyPairForSigning();

        System.out.println("Will generate a keypair. Press Enter to continue");
        in.readLine();

        KeyPair keyPair = elGamalKeyPairGenerator.generate();
        ElGamalPublicKey elGamalPublicKey = getElGamalPublicKey(keyPair);
        ElGamalPrivateKey elGamalPrivateKey = getElGamalPrivateKey(keyPair);

        BigInteger secretDataAsBigInt = new BigInteger(
            "7258566511205731274501076138955406501715314724101449712738362598579950386938540944887465860162123977709368083214223863972240202955142810928232980282975330370871308568085165077684085039876458285847574465974899160677249191425067809102830948294961345125633673938188727481916439054198157529653719706676063604935245024842269315556089246440255475308539173655418342524924565992764033784503570777559750521406592756973794259338361114583387726019915204757536641131719120008672199435739477691816523107124831533391498441341092740110948060646843197200688130802580784263720897279045752642595996003690127352386971518744986787935681");
        ZpGroupElement _secretDataAsGroupElement = new ZpGroupElement(secretDataAsBigInt, _group);

        CryptoAPIElGamalEncrypter encrypter = _elGamalServiceAPI.createEncrypter(elGamalPublicKey);
        List<ZpGroupElement> plaintext = new ArrayList<ZpGroupElement>();
        plaintext.add(_secretDataAsGroupElement);
        ElGamalEncrypterValues elGamalEncrypterValues = encrypter.encryptGroupElements(plaintext);

        System.out.println("Will split privatekey into shares. Press Enter to continue");
        in.readLine();

        ElGamalPrivateKeySharesService elGamalPrivateKeySharesService = new ElGamalPrivateKeySharesService();

        List<MultipleSharesContainer> containers =
            elGamalPrivateKeySharesService.split(elGamalPrivateKey, numShares, threshold);

        System.out.println("Number of shares: " + containers.size());

        System.out
            .println("Starting the process of writing to smartcards. Will write to " + numShares + " smartcards.");

        for (int i = 0; i < numShares; i++) {

            int indexToDisplay = i + 1;
            System.out.println("Please insert smartcard " + indexToDisplay + " and press Enter");
            in.readLine();

            String nameToWrite = name + "_" + i;
            _target.write(containers.get(i), nameToWrite, OLD_PIN_PUK, NEW_PIN_PUK, signAuthorityKeyPair.getPrivate());
        }

        System.out.println("Will begin to read data back from smartcards. Press Enter.");
        in.readLine();

        List<MultipleSharesContainer> readShares = new ArrayList<MultipleSharesContainer>();
        for (int i = 0; i < threshold; i++) {

            int indexToDisplay = i + 1;
            System.out.println("Please insert smartcard " + indexToDisplay + " and press Enter");
            in.readLine();

            MultipleSharesContainer c = _target.readElGamal(NEW_PIN_PUK, signAuthorityKeyPair.getPublic());
            readShares.add(c);
        }

        ElGamalPrivateKey recoveredPk = elGamalPrivateKeySharesService.recover(readShares, elGamalPublicKey);

        String errorMsg = "The original and the recovered private keys dont seem equal";
        assertEquals(errorMsg, elGamalPrivateKey, recoveredPk);

        CryptoAPIElGamalDecrypter decrypter = _elGamalServiceAPI.createDecrypter(elGamalPrivateKey);

        List<ZpGroupElement> decrypted = decrypter.decrypt(elGamalEncrypterValues.getComputationValues(), false);

        ZpGroupElement decryptedData = decrypted.get(0);
        System.out.println("original data: " + _secretDataAsGroupElement.getValue());
        System.out.println("decrypted data: " + decryptedData.getValue());

        errorMsg = "The decrypted data does not match the original data";
        assertEquals(errorMsg, _secretDataAsGroupElement, decryptedData);

    }

    private ElGamalPublicKey getElGamalPublicKey(KeyPair keyPair) {

        PublicKey publicKey = keyPair.getPublic();

        ElGamalPublicKeyAdapter elGamalPublicKeyAdapter = null;
        if (publicKey instanceof ElGamalPublicKeyAdapter) {
            elGamalPublicKeyAdapter = (ElGamalPublicKeyAdapter) publicKey;
        }

        return elGamalPublicKeyAdapter.getPublicKey();
    }

    private ElGamalPrivateKey getElGamalPrivateKey(KeyPair keyPair) {

        PrivateKey privateKey = keyPair.getPrivate();

        ElGamalPrivateKeyAdapter elGamalPrivateKeyAdapter = null;
        if (privateKey instanceof ElGamalPrivateKeyAdapter) {
            elGamalPrivateKeyAdapter = (ElGamalPrivateKeyAdapter) privateKey;
        }

        return elGamalPrivateKeyAdapter.getPrivateKey();
    }

}
