JavaKeyStore.java

/*
 * @copyright defined in LICENSE.txt
 */

package hera.keystore;

import static hera.util.ValidationUtils.assertNotNull;
import static java.util.Collections.list;
import static org.slf4j.LoggerFactory.getLogger;

import hera.annotation.ApiAudience;
import hera.annotation.ApiStability;
import hera.api.model.Authentication;
import hera.api.model.EncryptedPrivateKey;
import hera.api.model.Identity;
import hera.exception.InvalidAuthenticationException;
import hera.exception.KeyStoreException;
import hera.key.AergoKey;
import hera.key.Signer;
import hera.model.KeyAlias;
import hera.util.pki.ECDSAKey;
import hera.util.pki.ECDSAKeyGenerator;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.slf4j.Logger;

@ApiAudience.Public
@ApiStability.Unstable
public class JavaKeyStore implements KeyStore {

  protected final Logger logger = getLogger(getClass());

  protected final java.security.Provider bcProvider = new BouncyCastleProvider();

  protected volatile java.security.KeyStore delegate;

  /**
   * Create a keystore which uses {@link java.security.KeyStore}.
   *
   * @param delegate a java keystore
   *
   * @throws KeyStoreException on keystore error
   */
  public JavaKeyStore(final java.security.KeyStore delegate) {
    try {
      assertNotNull(delegate);
      this.delegate = delegate;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  /**
   * Create a keystore which uses {@link java.security.KeyStore}.
   *
   * @param type a keystore type see {@link java.security.KeyStore#getInstance(String)}
   *
   * @throws KeyStoreException on keystore error
   */
  public JavaKeyStore(final String type) {
    this(type, null, null);
  }

  /**
   * Create a keystore which uses {@link java.security.KeyStore}.
   *
   * @param type a keystore type see
   *        {@link java.security.KeyStore#getInstance(String, java.security.Provider)}
   * @param provider a keystore provider
   *
   * @throws KeyStoreException on keystore error
   */
  public JavaKeyStore(final String type, final java.security.Provider provider) {
    this(type, provider, null, null);
  }

  /**
   * Create a keystore which uses {@link java.security.KeyStore}.
   *
   * @param type a keystore type see {@link java.security.KeyStore#getInstance(String)}
   * @param inputStream an input stream for keystore
   * @param password a keystore password
   *
   * @throws KeyStoreException on keystore error
   */
  public JavaKeyStore(final String type, final InputStream inputStream, final char[] password) {
    try {
      assertNotNull(type, "Keystore type must not null");
      this.delegate = java.security.KeyStore.getInstance(type);
      this.delegate.load(inputStream, password);
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  /**
   * Create a keystore which uses {@link java.security.KeyStore}.
   *
   * @param type a keystore type see
   *        {@link java.security.KeyStore#getInstance(String, java.security.Provider)}
   * @param provider a keystore provider
   * @param inputStream an input stream for keystore
   * @param password a keystore password
   *
   * @throws KeyStoreException on keystore error
   */
  public JavaKeyStore(final String type, final java.security.Provider provider,
      final InputStream inputStream, final char[] password) {
    try {
      assertNotNull(type, "Keystore type must not null");
      assertNotNull(provider, "Keystore provider must not null");
      this.delegate = java.security.KeyStore.getInstance(type, provider);
      this.delegate.load(inputStream, password);
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  @Override
  public void save(final Authentication authentication, final AergoKey key) {
    try {
      logger.debug("Save key {} with authentication: {}", key, authentication);

      synchronized (this) {
        if (isExists(authentication)) {
          throw new InvalidAuthenticationException("Invalid authentication");
        }

        final String alias = authentication.getIdentity().getValue();
        final java.security.PrivateKey privateKey = key.getPrivateKey();
        final char[] rawPassword = authentication.getPassword().toCharArray();
        final Certificate cert = generateCertificate(key);
        final Certificate[] certChain = new Certificate[] {cert};
        this.delegate.setKeyEntry(alias, privateKey, rawPassword, certChain);
      }
    } catch (InvalidAuthenticationException e) {
      throw e;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  protected Certificate generateCertificate(final AergoKey key)
      throws OperatorCreationException, CertificateException {
    logger.trace("Generate certificate for account: {}", key);
    final Calendar start = Calendar.getInstance();
    final Calendar expiry = Calendar.getInstance();
    expiry.add(Calendar.YEAR, 1);
    final X500Name name = new X500Name("CN=" + key.getAddress().getValue());
    final ContentSigner signer = new JcaContentSignerBuilder("SHA256WithECDSA")
        .setProvider(bcProvider).build(key.getPrivateKey());
    final X509CertificateHolder holder = new X509v3CertificateBuilder(
        name, BigInteger.ONE, start.getTime(), expiry.getTime(), name,
        SubjectPublicKeyInfo.getInstance(key.getPublicKey().getEncoded()))
            .build(signer);
    final Certificate cert = new JcaX509CertificateConverter()
        .setProvider(bcProvider)
        .getCertificate(holder);
    logger.trace("Generated certificate: {}", cert);
    return cert;
  }

  @Override
  public Signer load(final Authentication authentication) {
    try {
      logger.debug("Load key with authentication: {}", authentication);

      synchronized (this) {
        if (false == isExists(authentication)) {
          throw new InvalidAuthenticationException("Invalid authentication");
        }

        final java.security.Key rawKey = loadRawKey(authentication);
        return convertPrivateKey(rawKey);
      }
    } catch (InvalidAuthenticationException e) {
      throw e;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  @Override
  public void remove(final Authentication authentication) {
    try {
      logger.debug("Export key with authentication: {}", authentication);

      synchronized (this) {
        if (false == isExists(authentication)) {
          throw new InvalidAuthenticationException("Invalid authentication");
        }

        final String alias = authentication.getIdentity().getValue();
        this.delegate.deleteEntry(alias);
      }
    } catch (InvalidAuthenticationException e) {
      throw e;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  @Override
  public EncryptedPrivateKey export(final Authentication authentication, final String password) {
    try {
      logger.debug("Export key with authentication: {}", authentication);

      synchronized (this) {
        if (false == isExists(authentication)) {
          throw new InvalidAuthenticationException("Invalid authentication");
        }

        final java.security.Key rawKey = loadRawKey(authentication);
        final AergoKey recovered = convertPrivateKey(rawKey);
        return recovered.export(password);
      }
    } catch (InvalidAuthenticationException e) {
      throw e;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  @Override
  public List<Identity> listIdentities() {
    try {
      final List<String> aliases = list(delegate.aliases());
      logger.trace("Aliases: {}", aliases);

      final List<Identity> storedIdentities = new ArrayList<>();
      for (final String alias : aliases) {
        storedIdentities.add(new KeyAlias(alias));
      }
      return storedIdentities;
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  @Override
  public void store(final String path, final char[] password) {
    try {
      this.delegate.store(new FileOutputStream(path), password);
    } catch (Exception e) {
      throw new KeyStoreException(e);
    }
  }

  protected boolean isExists(final Authentication authentication)
      throws java.security.KeyStoreException, NoSuchAlgorithmException {
    final String alias = authentication.getIdentity().getValue();
    if (this.delegate.containsAlias(alias)) {
      return true;
    }

    try {
      final java.security.Key rawKey = loadRawKey(authentication);
      if (null != rawKey) {
        return true;
      }
      return false;
    } catch (UnrecoverableKeyException e) {
      // decrypt failure
      return false;
    } catch (java.security.KeyStoreException | NoSuchAlgorithmException e) {
      throw e;
    }
  }

  protected java.security.Key loadRawKey(final Authentication authentication)
      throws UnrecoverableKeyException, java.security.KeyStoreException, NoSuchAlgorithmException {
    final String alias = authentication.getIdentity().getValue();
    final char[] rawPassword = authentication.getPassword().toCharArray();
    final java.security.Key rawKey = delegate.getKey(alias, rawPassword);
    return rawKey;
  }

  protected AergoKey convertPrivateKey(final java.security.Key privateKey) throws Exception {
    BigInteger d = null;
    if (privateKey instanceof java.security.interfaces.ECPrivateKey) {
      d = ((java.security.interfaces.ECPrivateKey) privateKey).getS();
    } else if (privateKey instanceof org.bouncycastle.jce.interfaces.ECPrivateKey) {
      d = ((org.bouncycastle.jce.interfaces.ECPrivateKey) privateKey).getD();
    } else {
      throw new UnsupportedOperationException(
          "Unacceptable key type: " + privateKey.getClass().getName());
    }
    final ECDSAKey ecdsakey = new ECDSAKeyGenerator().create(d);
    return new AergoKey(ecdsakey);
  }

}