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

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

import java.security.GeneralSecurityException;
import java.security.PublicKey;
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, it uses an encrypted secret key.
 */
public class EncryptedHMacEventProcessor extends HMacEventProcessor {

  private final PublicKey publicKey;
  private final int numberLogLines;
  private final long timerLogMilliseconds;

  /**
   * Constructor.
   *
   * @param currentHMac the current HMAC
   * @param secretKey the secret key
   * @param cipherPublicKey the cipher public key
   * @param cryptographyProvider the cryptography provider
   * @param numberLogLines the number of log lines
   * @param timerLogMilliseconds the timer log in milliseconds
   * @throws GeneralSecurityException failed to create an encrypted HMAC event processor.
   */
  public EncryptedHMacEventProcessor(final byte[] currentHMac, final SecretKey secretKey,
      final PublicKey cipherPublicKey, final SecureLoggerCryptographyProvider cryptographyProvider,
      int numberLogLines, long timerLogMilliseconds) throws GeneralSecurityException {
    super(currentHMac, secretKey, cryptographyProvider);
    publicKey = cipherPublicKey;
    this.numberLogLines = numberLogLines;
    this.timerLogMilliseconds = timerLogMilliseconds;
  }

  @Override
  public List<SecureLoggingEvent> secretKeyLog(final SecurePatternLayout layout)
      throws EventProcessorException {
    HMacSpec spec = new HMacSpec(layout);
    spec.event = SecureLoggingEvent.newHMacInstance();
    Map<String, String> properties = new LinkedHashMap<>();
    spec.liberatedKey = addLiberatedSessionSecretKey(properties);
    initializeHMac();
    spec.encryptedKey = addEncryptedSessionSecretKey(properties);
    addPreviousEntryHMac(properties);
    addNumberLogLines(properties);
    addTimerLogMilliseconds(properties);
    spec.timestamp = addTimestamp(properties);
    addHMac(properties, spec);
    List<SecureLoggingEvent> events = new LinkedList<>();
    events.add(spec.event.addProperties(properties));
    return events;
  }

  @Override
  public List<SecureLoggingEvent> stopProcessor(final SecurePatternLayout layout)
      throws EventProcessorException {
    return secretKeyLog(layout);
  }

  private byte[] addEncryptedSessionSecretKey(Map<String, String> messageProperties)
      throws EventProcessorException {
    byte[] encryptedSecretKeyBytes = encryptSecretKey();
    String encryptedSecretKey = Base64.getEncoder().encodeToString(encryptedSecretKeyBytes);
    messageProperties.put(SecureMessageProperties.ENCRYPTED_SESSION_KEY, encryptedSecretKey);
    return encryptedSecretKeyBytes;
  }

  private void addHMac(Map<String, String> messageProperties, HMacSpec spec)
      throws EventProcessorException {
    String hmac;
    try {
      hmac = calculateHMac(spec.event, spec.layout, spec.liberatedKey, spec.encryptedKey,
          numberLogLines, timerLogMilliseconds, spec.timestamp);
    } catch (GeneralSecurityException e) {
      throw new EventProcessorException("Failed to add HMAC.", e);
    }
    messageProperties.put(SecureMessageProperties.HMAC, hmac);
  }

  private byte[] addLiberatedSessionSecretKey(Map<String, String> messageProperties) {
    if (getSecretKey() != null) {
      byte[] secretKeyBytes = getSecretKey().getEncoded();
      String liberatedSecretKey = Base64.getEncoder().encodeToString(secretKeyBytes);
      if (liberatedSecretKey.length() > 0) {
        messageProperties.put(SecureMessageProperties.LIBERATED_SESSION_KEY, liberatedSecretKey);
        return secretKeyBytes;
      }
    }
    return new byte[0];
  }

  private void addNumberLogLines(Map<String, String> messageProperties) {
    String value = Integer.toString(numberLogLines);
    messageProperties.put(SecureMessageProperties.NUMBER_LOG_LINES, value);
  }

  private void addPreviousEntryHMac(Map<String, String> messageProperties) {
    if (getLastHmac() != null && !getLastHmac().isEmpty()) {
      messageProperties.put(SecureMessageProperties.PHMAC, getLastHmac());
    }
  }

  private void addTimerLogMilliseconds(Map<String, String> messageProperties) {
    String value = Long.toString(timerLogMilliseconds);
    messageProperties.put(SecureMessageProperties.TIMER_LOG_MILLISECONDS, value);
  }

  private long addTimestamp(Map<String, String> messageProperties) {
    long timestamp = System.currentTimeMillis();
    messageProperties.put(SecureMessageProperties.TIMESTAMP, Long.toString(timestamp));
    return timestamp;
  }

  private byte[] encryptSecretKey() throws EventProcessorException {
    try {
      return getCryptographyProvider()
          .asymmetricEncrypt(Base64.getEncoder().encode(getSecretKey().getEncoded()), publicKey);
    } catch (GeneralSecurityException e) {
      throw new EventProcessorException(e);
    }
  }

  private static final class HMacSpec {
    private final SecurePatternLayout layout;
    private SecureLoggingEvent event;
    private byte[] liberatedKey;
    private byte[] encryptedKey;
    private long timestamp;

    public HMacSpec(SecurePatternLayout layout) {
      this.layout = layout;
    }
  }
}
