/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {SecurityException} from '../exception/security_exception';

import * as Bytes from './bytes';
import * as EllipticCurves from './elliptic_curves';
import * as Hkdf from './hkdf';

/**
 * HKDF-based ECIES-KEM (key encapsulation mechanism) for ECIES sender.
 */
export class EciesHkdfKemSender {
  private readonly publicKey: CryptoKey;

  constructor(recipientPublicKey: CryptoKey) {
    if (!recipientPublicKey) {
      throw new SecurityException('Recipient public key has to be non-null.');
    }

    // CryptoKey should have the properties type and algorithm.
    if (recipientPublicKey.type !== 'public' || !recipientPublicKey.algorithm) {
      throw new SecurityException('Expected Crypto key of type: public.');
    }
    this.publicKey = recipientPublicKey;
  }

  /**
   * @param keySizeInBytes The length of the generated pseudorandom
   *     string in bytes. The maximal size is 255 * DigestSize, where DigestSize
   *     is the size of the underlying HMAC.
   * @param pointFormat The format of the
   *     public ephemeral point.
   * @param hkdfHash the name of the hash function. Accepted names are
   *     SHA-1, SHA-256 and SHA-512.
   * @param hkdfInfo Context and application specific
   *     information (can be a zero-length array).
   * @param opt_hkdfSalt Salt value (a non-secret random
   *     value). If not provided, it is set to a string of hash length zeros.
   * @return The KEM key and
   *     token.
   */
  async encapsulate(
      keySizeInBytes: number, pointFormat: EllipticCurves.PointFormatType,
      hkdfHash: string, hkdfInfo: Uint8Array, opt_hkdfSalt?: Uint8Array):
      Promise<{key: Uint8Array, token: Uint8Array}> {
    const {namedCurve}: Partial<EcKeyAlgorithm> = this.publicKey.algorithm;
    if (!namedCurve) {
      throw new SecurityException('Curve has to be defined.');
    }
    const ephemeralKeyPair =
        await EllipticCurves.generateKeyPair('ECDH', namedCurve);
    const sharedSecret = await EllipticCurves.computeEcdhSharedSecret(
        ephemeralKeyPair.privateKey!, this.publicKey);
    const jwk =
        await EllipticCurves.exportCryptoKey(ephemeralKeyPair.publicKey!);
    const {crv} = jwk;
    if (!crv) {
      throw new SecurityException('Curve has to be defined.');
    }
    const kemToken = EllipticCurves.pointEncode(crv, pointFormat, jwk);
    const hkdfIkm = Bytes.concat(kemToken, sharedSecret);
    const kemKey = await Hkdf.compute(
        keySizeInBytes, hkdfHash, hkdfIkm, hkdfInfo, opt_hkdfSalt);
    return {'key': kemKey, 'token': kemToken};
  }
}

export async function fromJsonWebKey(jwk: JsonWebKey):
    Promise<EciesHkdfKemSender> {
  const publicKey = await EllipticCurves.importPublicKey('ECDH', jwk);
  return new EciesHkdfKemSender(publicKey);
}
