/*
 * 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.slogger.crypto;

import static java.util.Objects.requireNonNull;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.stores.StoresServiceAPI;
import com.scytl.cryptolib.api.stores.bean.KeyStoreType;
import com.scytl.cryptolib.api.symmetric.SymmetricServiceAPI;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIDerivedKey;

import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Enumeration;

import javax.crypto.SecretKey;

/**
 * Implementation of {@link SecureLoggerCryptographyProvider} based on the Cryptolib.
 *
 * @author aakimov
 */
public final class CryptolibSecureLoggerCryptographyProvider
    implements SecureLoggerCryptographyProvider {
  private final AsymmetricServiceAPI asymmetricService;
  private final SymmetricServiceAPI symmetricService;
  private final StoresServiceAPI storesService;

  /**
   * Constructor.
   *
   * @param asymmetricService the asymmetric service
   * @param symmetricService the symmetric service
   * @param storesService the stores service.
   */
  public CryptolibSecureLoggerCryptographyProvider(AsymmetricServiceAPI asymmetricService,
      SymmetricServiceAPI symmetricService, StoresServiceAPI storesService) {
    this.asymmetricService = asymmetricService;
    this.symmetricService = symmetricService;
    this.storesService = storesService;
  }

  private static PrivateKey getPrivateKey(KeyStore store, char[] password)
      throws GeneralSecurityException {
    Enumeration<String> aliases = store.aliases();
    while (aliases.hasMoreElements()) {
      String alias = aliases.nextElement();
      if (store.entryInstanceOf(alias, PrivateKeyEntry.class)) {
        return (PrivateKey) store.getKey(alias, password);
      }
    }
    return null;
  }

  private static PublicKey getPublicKey(KeyStore store) throws GeneralSecurityException {
    Enumeration<String> aliases = store.aliases();
    while (aliases.hasMoreElements()) {
      String alias = aliases.nextElement();
      if (store.entryInstanceOf(alias, PrivateKeyEntry.class)) {
        return store.getCertificate(alias).getPublicKey();
      }
    }
    return null;
  }

  @Override
  public byte[] asymmetricDecrypt(byte[] data, PrivateKey privateKey)
      throws GeneralSecurityException {
    requireNonNull(data, "Data is null.");
    requireNonNull(privateKey, "Private key is null.");
    try {
      return asymmetricService.decrypt(privateKey, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to decrypt.", e);
    }
  }

  @Override
  public byte[] asymmetricEncrypt(byte[] data, PublicKey publicKey)
      throws GeneralSecurityException {
    requireNonNull(data, "Data is null.");
    requireNonNull(publicKey, "Public key is null.");
    try {
      return asymmetricService.encrypt(publicKey, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to encrypt.", e);
    }
  }

  @Override
  public SecretKey generateSymmetricKey() {
    return symmetricService.getSecretKeyForHmac();
  }

  @Override
  public SecretKey generateSymmetricKey(byte[] data) throws GeneralSecurityException {
    requireNonNull(data, "Data is null.");
    try {
      return symmetricService.getSecretKeyForMacFromDerivedKey(new DerivedKey(data));
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to generate symmetric key.", e);
    }
  }

  @Override
  public byte[] getMac(SecretKey key, byte[]... data) throws GeneralSecurityException {
    requireNonNull(key, "Key is null.");
    requireNonNull(data, "Data is null.");
    for (byte[] part : data) {
      requireNonNull(part, "Data part is null.");
    }
    try {
      return symmetricService.getMac(key, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to get MAC.", e);
    }
  }

  @Override
  public PrivateKey getUserCipherPrivateKey(SecureLoggerKeyStore keystore, char[] password)
      throws GeneralSecurityException {
    requireNonNull(keystore, "Key store is null.");
    requireNonNull(password, "Password is null.");
    return getPrivateKey(keystore.getKeyStore(), password);
  }

  @Override
  public PublicKey getUserCipherPublicKey(SecureLoggerKeyStore keystore)
      throws GeneralSecurityException {
    requireNonNull(keystore, "Key store is null.");
    return getPublicKey(keystore.getKeyStore());
  }

  @Override
  public PrivateKey getUserSignaturePrivateKey(SecureLoggerKeyStore keystore, char[] password)
      throws GeneralSecurityException {
    requireNonNull(keystore, "Key store is null.");
    requireNonNull(password, "Password is null.");
    return getPrivateKey(keystore.getKeyStore(), password);
  }

  @Override
  public PublicKey getUserSignaturePublicKey(SecureLoggerKeyStore keystore)
      throws GeneralSecurityException {
    requireNonNull(keystore, "Key store is null.");
    return getPublicKey(keystore.getKeyStore());
  }

  @Override
  public void init() throws GeneralSecurityException {}

  @Override
  public Certificate readCertificate(InputStream stream) throws GeneralSecurityException {
    return CertificateFactory.getInstance("X.509").generateCertificate(stream);
  }

  @Override
  public SecureLoggerKeyStore readKeyStore(InputStream stream, char[] password)
      throws IOException, GeneralSecurityException {
    KeyStore store;
    try {
      store = storesService.loadKeyStore(KeyStoreType.PKCS12, stream, password);
    } catch (GeneralCryptoLibException e) {
      if (e.getCause() instanceof IOException) {
        throw (IOException) e.getCause();
      }
      throw new GeneralSecurityException("Failed to read key store.", e);
    }
    return new DefaultSecureLoggerKeyStore(store);
  }

  @Override
  public byte[] sign(byte[] data, PrivateKey privateKey) throws GeneralSecurityException {
    try {
      return asymmetricService.sign(privateKey, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to sign.", e);
    }
  }

  @Override
  public boolean verify(byte[] data, byte[] signature, PublicKey publicKey)
      throws GeneralSecurityException {
    try {
      return asymmetricService.verifySignature(signature, publicKey, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to verify signature.", e);
    }
  }

  @Override
  public boolean verifyMac(SecretKey key, byte[] mac, byte[]... data)
      throws GeneralSecurityException {
    requireNonNull(key, "Key is null.");
    requireNonNull(mac, "MAC is null.");
    requireNonNull(data, "Data is null.");
    for (byte[] part : data) {
      requireNonNull(part, "Data part is null.");
    }
    try {
      return symmetricService.verifyMac(key, mac, data);
    } catch (GeneralCryptoLibException e) {
      throw new GeneralSecurityException("Failed to get MAC.", e);
    }
  }

  private static class DerivedKey implements CryptoAPIDerivedKey {
    private final byte[] data;

    public DerivedKey(byte[] data) {
      this.data = data.clone();
    }

    @Override
    public byte[] getEncoded() {
      return data;
    }
  }
}
