ProductKeyChain.java

/*
 * ProductPublicKey
 */
package gov.usgs.earthquake.distribution;

import gov.usgs.earthquake.product.ProductId;
import gov.usgs.util.Config;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.LinkedList;
import java.util.logging.Logger;

import org.yaml.snakeyaml.Yaml;

/**
 * A group of keys that can be used to verify product signatures.
 */
public class ProductKeyChain {

  /** Logging object. */
  private static final Logger LOGGER = Logger.getLogger(ProductKeyChain.class.getName());

  /** List of candidate keys. */
  private List<ProductKey> keychain = new LinkedList<ProductKey>();

  /** Empty constructor */
  public ProductKeyChain() {
  }

  /**
   * Constructor for a string of keys
   *
   * @param keys   String of keys, separated by commas
   * @param config Config file
   * @throws Exception if error occurs
   */
  public ProductKeyChain(final String keys, final Config config) throws Exception {
    this(StringUtils.split(keys, ","), config);
  }

  /**
   * Constructor for list of keys
   *
   * @param keys   String list of keys
   * @param config Config file
   * @throws Exception if error occurs
   */
  public ProductKeyChain(final List<String> keys, final Config config) throws Exception {
    Iterator<String> iter = keys.iterator();
    while (iter.hasNext()) {
      String keyName = iter.next();
      LOGGER.config("Loading key '" + keyName + "'");
      ProductKey key = (ProductKey) config.getObject(keyName);

      if (key != null) {
        keychain.add(key);
      }
    }
  }

  /**
   * Static constructor to create from a file
   *
   * @param keychainFile  File to use
   * @param propertyStart The property to look for the keychain from
   * @throws Exception if error occurs
   */
  public static ProductKeyChain fromFile(final File keychainFile, final String propertyStart) throws Exception {
    String extension = Optional.ofNullable(keychainFile.getName())
        .filter(f -> f.contains("."))
        .map(f -> f.substring(keychainFile.getName().lastIndexOf(".") + 1))
        .orElse("");

    if (extension.equals("ini")) {
      return fromIniFile(keychainFile, propertyStart);
    } else if (extension.equals("yml") || extension.equals("yaml")) {
      return fromYmlFile(keychainFile, propertyStart);
    } else {
      LOGGER.warning(() -> String.format("Unsupported file extension %s for keychain file", extension));
      throw new IllegalArgumentException(String.format("Unsupported file extension %s for keychain file", extension));
    }
  }

  /**
   * Static constructor create from an ini file
   *
   * @param keychainFile  File to use
   * @param propertyStart The property to look for the keychain from
   * @throws IOException if the file does not exist
   * @throws Exception   if error occurs
   */
  protected static ProductKeyChain fromIniFile(final File keychainFile, final String propertyStart) throws Exception {
    Config keychainConfig = new Config();

    try (InputStream in = StreamUtils.getInputStream(keychainFile)) {
      keychainConfig.load(in);
    }
    String keyNames = keychainConfig.getProperty(propertyStart);
    return new ProductKeyChain(keyNames, keychainConfig);
  }

  /**
   * Static constructor create from an yml file
   *
   * @param keychainFile  File to use
   * @param propertyStart The property to look for the keychain from
   * @throws IOException if the file does not exist
   * @throws Exception   if error occurs
   */
  @SuppressWarnings("unchecked")
  protected static ProductKeyChain fromYmlFile(final File keychainFile, final String propertyStart) throws Exception {
    Map<String, Object> yaml;
    try (InputStream in = StreamUtils.getInputStream(keychainFile)) {
      yaml = new Yaml().load(in);
    }

    List<String> keyNames = new ArrayList<>();
    Config config = new Config();

    Map<String, Object> keychain = (Map<String, Object>) yaml.get(propertyStart);
    if (Objects.isNull(keychain)) {
      throw new IllegalArgumentException(String.format("Keychain file does not contain %s property", propertyStart));
    }

    for (Map.Entry<String, Object> entry : keychain.entrySet()) {

      Map<String, Object> keyObject = (Map<String, Object>) entry.getValue();

      List<String> sources = keyObject.containsKey("sources") ? (List<String>) keyObject.get("sources") : List.of();
      List<String> types = keyObject.containsKey("types") ? (List<String>) keyObject.get("types") : List.of();

      List<String> keys = (List<String>) keyObject.get("keys");
      if (Objects.isNull(keys)) {
        throw new IllegalArgumentException("Keychain file does not contain keys property");
      }

      for (int i = 0; i < keys.size(); i++) {
        String key = entry.getKey() + "_" + i;
        config.setSectionProperty(key, ProductKey.KEY_PROPERTY_NAME, keys.get(i));
        config.setSectionProperty(key, ProductKey.SOURCES_PROPERTY_NAME, String.join(",", sources));
        config.setSectionProperty(key, ProductKey.TYPES_PROPERTY_NAME, String.join(",", types));
        config.setSectionProperty(key, Config.OBJECT_TYPE_PROPERTY, ProductKey.class.getName());

        keyNames.add(key);
      }
    }
    return new ProductKeyChain(keyNames, config);

  }

  /**
   * @return the keys
   */
  public List<ProductKey> getKeychain() {
    return keychain;
  }

  /**
   * Find public keys based on configured Keys.
   *
   * @param id ID of product
   * @return an array of candidate keys used to verify a signature.
   */
  public PublicKey[] getProductKeys(final ProductId id) {
    LinkedList<PublicKey> publicKeys = new LinkedList<PublicKey>();
    Iterator<ProductKey> iter = keychain.iterator();
    while (iter.hasNext()) {
      ProductKey key = iter.next();
      if (key.isForProduct(id)) {
        publicKeys.add(key.getKey());
      }
    }
    return publicKeys.toArray(new PublicKey[0]);
  }

}