/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {Aead} from '../aead/internal/aead';
import {SecurityException} from '../exception/security_exception';
import {HybridDecrypt} from '../hybrid/internal/hybrid_decrypt';

import {EciesAeadHkdfDemHelper} from './ecies_aead_hkdf_dem_helper';
import {EciesHkdfKemRecipient, fromJsonWebKey as kemRecipientFromJsonWebKey} from './ecies_hkdf_kem_recipient';
import * as EllipticCurves from './elliptic_curves';

/**
 * Implementation of ECIES AEAD HKDF hybrid decryption.
 *
 * @final
 */
export class EciesAeadHkdfHybridDecrypt extends HybridDecrypt {
  private readonly kemRecipient_: EciesHkdfKemRecipient;
  private readonly hkdfHash_: string;
  private readonly pointFormat_: EllipticCurves.PointFormatType;
  private readonly demHelper_: EciesAeadHkdfDemHelper;
  private readonly headerSize: number;
  private readonly hkdfSalt: Uint8Array|undefined;

  /**
   * @param hkdfHash the name of the HMAC algorithm, accepted names
   *     are: SHA-1, SHA-256 and SHA-512.
   */
  constructor(
      recipientPrivateKey: JsonWebKey, kemRecipient: EciesHkdfKemRecipient,
      hkdfHash: string, pointFormat: EllipticCurves.PointFormatType,
      demHelper: EciesAeadHkdfDemHelper, opt_hkdfSalt?: Uint8Array) {
    super();
    if (!recipientPrivateKey) {
      throw new SecurityException('Recipient private key has to be non-null.');
    }
    if (!kemRecipient) {
      throw new SecurityException('KEM recipient has to be non-null.');
    }
    if (!hkdfHash) {
      throw new SecurityException('HKDF hash algorithm has to be non-null.');
    }
    if (!pointFormat) {
      throw new SecurityException('Point format has to be non-null.');
    }
    if (!demHelper) {
      throw new SecurityException('DEM helper has to be non-null.');
    }
    const {crv} = recipientPrivateKey;
    if (!crv) {
      throw new SecurityException('Curve has to be defined.');
    }
    const curveType = EllipticCurves.curveFromString(crv);
    const headerSize =
        EllipticCurves.encodingSizeInBytes(curveType, pointFormat);
    this.kemRecipient_ = kemRecipient;
    this.hkdfHash_ = hkdfHash;
    this.pointFormat_ = pointFormat;
    this.demHelper_ = demHelper;
    this.headerSize = headerSize;
    this.hkdfSalt = opt_hkdfSalt;
  }

  /**
   * Decrypts ciphertext using opt_contextInfo as info parameter of the
   * underlying HKDF.
   *
   */
  async decrypt(ciphertext: Uint8Array, associatedData?: Uint8Array) {
    if (ciphertext.length < this.headerSize) {
      throw new SecurityException('Ciphertext is too short.');
    }

    // Split the ciphertext to KEM token and AEAD ciphertext.
    const kemToken = ciphertext.slice(0, this.headerSize);
    const ciphertextBody = ciphertext.slice(this.headerSize, ciphertext.length);
    const aead = await this.getAead(kemToken, associatedData);
    return aead.decrypt(ciphertextBody);
  }

  private async getAead(
      kemToken: Uint8Array, opt_contextInfo?: Uint8Array|null): Promise<Aead> {
    // Variable hkdfInfo is not optional for decapsulate method. Thus it should
    // be an empty array in case that it is not defined by the caller of decrypt
    // method.
    if (!opt_contextInfo) {
      opt_contextInfo = new Uint8Array(0);
    }
    const symmetricKey = await this.kemRecipient_.decapsulate(
        kemToken, this.demHelper_.getDemKeySizeInBytes(), this.pointFormat_,
        this.hkdfHash_, opt_contextInfo, this.hkdfSalt);
    return this.demHelper_.getAead(symmetricKey);
  }
}

/**
 * @param hkdfHash the name of the HMAC algorithm, accepted names
 *     are: SHA-1, SHA-256 and SHA-512.
 */
export async function fromJsonWebKey(
    recipientPrivateKey: JsonWebKey, hkdfHash: string,
    pointFormat: EllipticCurves.PointFormatType,
    demHelper: EciesAeadHkdfDemHelper,
    opt_hkdfSalt?: Uint8Array): Promise<HybridDecrypt> {
  if (!recipientPrivateKey) {
    throw new SecurityException('Recipient private key has to be non-null.');
  }
  if (!hkdfHash) {
    throw new SecurityException('HKDF hash algorithm has to be non-null.');
  }
  if (!pointFormat) {
    throw new SecurityException('Point format has to be non-null.');
  }
  if (!demHelper) {
    throw new SecurityException('DEM helper has to be non-null.');
  }
  if (!recipientPrivateKey) {
    throw new SecurityException('Recipient private key has to be non-null.');
  }
  const kemRecipient = await kemRecipientFromJsonWebKey(recipientPrivateKey);
  return new EciesAeadHkdfHybridDecrypt(
      recipientPrivateKey, kemRecipient, hkdfHash, pointFormat, demHelper,
      opt_hkdfSalt);
}
