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

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.processor.BaseLoggingEventProcessor;
import com.scytl.slogger.processor.EventProcessorException;

import org.apache.log4j.spi.LoggingEvent;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.crypto.SecretKey;

/**
 * Implementation of event processor with hmac chained logs.
 */
public abstract class HMacEventProcessor extends BaseLoggingEventProcessor {

  private byte[] lastHmac;

  private SecretKey secretKey;

  private final SecureLoggerCryptographyProvider cryptographyProvider;

  /**
   * Constructor.
   *
   * @param currentHMac the current HMAC
   * @param secretKey the secret key
   * @param cryptographyProvider the cryptography provider
   * @throws NoSuchAlgorithmException failed to get HMAC instance
   * @throws GeneralSecurityException failed to create an instance.
   */
  public HMacEventProcessor(final byte[] currentHMac, final SecretKey secretKey,
      final SecureLoggerCryptographyProvider cryptographyProvider) throws GeneralSecurityException {
    this.cryptographyProvider = cryptographyProvider;
    lastHmac = currentHMac.clone();
    this.secretKey = secretKey;
  }

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

  /**
   * Returns the last HMAC encoded in Base64.
   *
   * @return the last HMAC encoded in Base64.
   */
  public String getLastHmac() {
    return Base64.getEncoder().encodeToString(lastHmac);
  }

  /**
   * Returns the secret key.
   *
   * @return the secret key.
   */
  public SecretKey getSecretKey() {
    return secretKey;
  }

  @Override
  public List<SecureLoggingEvent> processEvent(final SecureLoggingEvent loggingEvent,
      final SecurePatternLayout layout) throws EventProcessorException {
    Map<String, String> messageProperties = new LinkedHashMap<>();
    long timestamp = System.currentTimeMillis();
    messageProperties.put(SecureMessageProperties.TIMESTAMP, Long.toString(timestamp));
    String hmac;
    try {
      hmac = calculateHMac(loggingEvent, layout, timestamp);
    } catch (GeneralSecurityException e) {
      throw new EventProcessorException("Failed to process event.", e);
    }
    messageProperties.put(SecureMessageProperties.HMAC, hmac);
    List<SecureLoggingEvent> loggingEventList = new LinkedList<>();
    loggingEventList.add(loggingEvent.addProperties(messageProperties));
    return loggingEventList;
  }

  /**
   * Log the HMAC secret key.
   *
   * @param layout the layout
   * @return the list of events.
   */
  public abstract List<SecureLoggingEvent> secretKeyLog(final SecurePatternLayout layout)
      throws EventProcessorException;

  @Override
  public List<SecureLoggingEvent> startProcessor(final SecurePatternLayout layout)
      throws EventProcessorException {
    LinkedList<SecureLoggingEvent> loggingEventList = new LinkedList<>();
    loggingEventList.addAll(secretKeyLog(layout));
    return loggingEventList;
  }

  /**
   * Calculates HMAC.
   *
   * @param loggingEvent the event
   * @param layout the layout
   * @param liberatedSessionSecretKey the liberated session secret key
   * @param encryptedSessionSecretKey the encrypted session secret key.
   * @param numberLogLines the number of log lines
   * @param timerLogMilliseconds the timer log in milliseconds
   * @param timestamp the timestamp
   * @return the HMAC
   * @throws GeneralSecurityException failed to calculate HMAC.
   */
  protected final String calculateHMac(final LoggingEvent loggingEvent,
      final SecurePatternLayout layout, byte[] liberatedSessionSecretKey,
      byte[] encryptedSessionSecretKey, int numberLogLines, long timerLogMilliseconds,
      long timestamp) throws GeneralSecurityException {
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    try (DataOutputStream stream = new DataOutputStream(bytes)) {
      stream.write(lastHmac);
      stream.write(liberatedSessionSecretKey);
      stream.write(encryptedSessionSecretKey);
      stream.writeInt(numberLogLines);
      stream.writeLong(timerLogMilliseconds);
      stream.writeLong(timestamp);
      stream.write(layout.format(loggingEvent).getBytes(StandardCharsets.UTF_8));
    } catch (IOException e) {
      throw new IllegalStateException("Failed to compose data for HMAC.", e);
    }
    lastHmac = cryptographyProvider.getMac(secretKey, bytes.toByteArray());
    return getLastHmac();
  }

  /**
   * Calculates HMAC.
   *
   * @param loggingEvent the event
   * @param layout the layout
   * @return the HMAC.
   * @throws GeneralSecurityException failed to calculate HMAC.
   */
  protected final String calculateHMac(final LoggingEvent loggingEvent,
      final SecurePatternLayout layout, long timestamp) throws GeneralSecurityException {
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    try (DataOutputStream stream = new DataOutputStream(bytes)) {
      stream.write(lastHmac);
      stream.writeLong(timestamp);
      stream.write(layout.format(loggingEvent).getBytes(StandardCharsets.UTF_8));
    } catch (IOException e) {
      throw new IllegalStateException("Failed to compose data for HMAC.", e);
    }
    lastHmac = cryptographyProvider.getMac(secretKey, bytes.toByteArray());
    return getLastHmac();
  }

  /**
   * Initializes the HMAC.
   *
   * @throws EventProcessorException failed to initialize the HMAC.
   */
  protected final void initializeHMac() throws EventProcessorException {
    try {
      secretKey = cryptographyProvider.generateSymmetricKey();
    } catch (GeneralSecurityException e) {
      throw new EventProcessorException(e);
    }
  }
}
