/*
 * 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.validator.hmac;

import static java.text.MessageFormat.format;

import com.scytl.slogger.crypto.SecureLoggerCryptographyProvider;
import com.scytl.slogger.event.SecureLoggingEvent;
import com.scytl.slogger.event.SecureMessageProperties;
import com.scytl.slogger.event.SecurePatternLayout;
import com.scytl.slogger.validator.EventValidatorException;
import com.scytl.slogger.validator.LoggingEventValidator;

import org.apache.log4j.Layout;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;

import javax.crypto.SecretKey;

/**
 * Implementation of event validator to verify hmac chained logs.
 */
public abstract class HMacEventValidator implements LoggingEventValidator {
  private final SecureLoggerCryptographyProvider cryptographyProvider;

  private SecretKey secretKey;

  private byte[] lastHmac = {};

  /**
   * Constructor.
   *
   * @param cryptographyProvider the cryptography provider.
   */
  public HMacEventValidator(final SecureLoggerCryptographyProvider cryptographyProvider) {
    this.cryptographyProvider = cryptographyProvider;
  }

  /**
   * Returns the cryptography provider.
   *
   * @return the cryptographyProvider.
   */
  public SecureLoggerCryptographyProvider getCryptographyProvider() {
    return cryptographyProvider;
  }

  public byte[] getLastHmac() {
    return lastHmac.clone();
  }

  @Override
  public void stopValidation(final SecurePatternLayout layout) throws EventValidatorException {}

  @Override
  public void validateEvent(final SecureLoggingEvent event, final SecurePatternLayout layout)
      throws EventValidatorException {
    initializeHMac(event, layout);

    Map<String, String> properties = event.getSecureMessage().properties();
    String mac = properties.get(SecureMessageProperties.HMAC);
    if (mac == null) {
      throw new EventValidatorException("Unable to find the HMAC", event, layout);
    }
    byte[] currentEventHMac = decodeBase64(mac);

    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    try (DataOutputStream stream = new DataOutputStream(bytes)) {
      stream.write(lastHmac);
      String liberatedSessionSecretKey =
          properties.get(SecureMessageProperties.LIBERATED_SESSION_KEY);
      stream.write(decodeBase64(liberatedSessionSecretKey));
      String encryptedSessionSecretKey =
          properties.get(SecureMessageProperties.ENCRYPTED_SESSION_KEY);
      stream.write(decodeBase64(encryptedSessionSecretKey));
      Integer numberLogLines = getNumberLogLines(event, layout);
      if (numberLogLines != null) {
        stream.writeInt(numberLogLines.intValue());
      }
      Long timerLogMilliseconds = getTimerLogMilliseconds(event, layout);
      if (timerLogMilliseconds != null) {
        stream.writeLong(timerLogMilliseconds.longValue());
      }
      Long timestamp = getTimestamp(event, layout);
      if (timestamp != null) {
        stream.writeLong(timestamp.longValue());
      }
      stream.write(layout.format(event, false).getBytes(StandardCharsets.UTF_8));
    } catch (IOException e) {
      throw new IllegalStateException("Failed to compose data for HMAC.", e);
    }
    try {
      lastHmac = cryptographyProvider.getMac(secretKey, bytes.toByteArray());
    } catch (GeneralSecurityException e) {
      lastHmac = currentEventHMac;
      throw new EventValidatorException("The HMac validation has failed", event, layout, e);
    }
    if (!Arrays.equals(lastHmac, currentEventHMac)) {
      lastHmac = currentEventHMac;
      throw new EventValidatorException("The HMac validation has failed", event, layout);
    }
  }

  /**
   * Initializes HMAC.
   *
   * @param event the event
   * @param layout the layout
   * @throws EventValidatorException failed to initialize HMAC.
   */
  protected abstract void initializeHMac(final SecureLoggingEvent event, final Layout layout)
      throws EventValidatorException;

  /**
   * Loads HMAC secret key.
   *
   * @param secretKey the secret key
   * @throws NoSuchAlgorithmException the algorithm is not supported
   * @throws InvalidKeyException the key is invalid
   * @throws NoSuchProviderException the provider does not exist.
   */
  protected final void loadHMacSecretKey(final SecretKey secretKey)
      throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
    this.secretKey = secretKey;
  }

  private byte[] decodeBase64(String value) {
    if (value == null || value.isEmpty()) {
      return new byte[0];
    }
    return Base64.getDecoder().decode(value);
  }

  private Integer getNumberLogLines(SecureLoggingEvent event, SecurePatternLayout layout)
      throws EventValidatorException {
    String property = 
        event.getSecureMessage().properties().get(SecureMessageProperties.NUMBER_LOG_LINES);
    if (property == null) {
      return null;
    }
    try {
      return Integer.valueOf(property);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid LN: {0})", property), event, layout);
    }
  }

  private Long getTimerLogMilliseconds(SecureLoggingEvent event, SecurePatternLayout layout)
      throws EventValidatorException {
    String property =
        event.getSecureMessage().properties().get(SecureMessageProperties.TIMER_LOG_MILLISECONDS);
    if (property == null) {
      return null;
    }
    try {
      return Long.valueOf(property);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid TL: {0})", property), event, layout);
    }
  }

  private Long getTimestamp(SecureLoggingEvent event, SecurePatternLayout layout)
      throws EventValidatorException {
    String property = 
        event.getSecureMessage().properties().get(SecureMessageProperties.TIMESTAMP);
    if (property == null) {
      return null;
    }
    try {
      return Long.valueOf(property);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid TS: {0})", property), event, layout);
    }
  }
}
