// GPG4Browsers - An OpenPGP implementation in javascript
// Copyright (C) 2011 Recurity Labs GmbH
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

import PublicKeyPacket from './public_key';
import { newS2KFromConfig, newS2KFromType } from '../type/s2k';
import crypto from '../crypto';
import enums from '../enums';
import util from '../util';
import defaultConfig from '../config';
import { UnsupportedError, writeTag } from './packet';
import computeHKDF from '../crypto/hkdf';

/**
 * A Secret-Key packet contains all the information that is found in a
 * Public-Key packet, including the public-key material, but also
 * includes the secret-key material after all the public-key fields.
 * @extends PublicKeyPacket
 */
class SecretKeyPacket extends PublicKeyPacket {
  static get tag() {
    return enums.packet.secretKey;
  }

  /**
   * @param {Date} [date] - Creation date
   * @param {Object} [config] - Full configuration, defaults to openpgp.config
   */
  constructor(date = new Date(), config = defaultConfig) {
    super(date, config);
    /**
     * Secret-key data
     */
    this.keyMaterial = null;
    /**
     * Indicates whether secret-key data is encrypted. `this.isEncrypted === false` means data is available in decrypted form.
     */
    this.isEncrypted = null;
    /**
     * S2K usage
     * @type {enums.symmetric}
     */
    this.s2kUsage = 0;
    /**
     * S2K object
     * @type {type/s2k}
     */
    this.s2k = null;
    /**
     * Symmetric algorithm to encrypt the key with
     * @type {enums.symmetric}
     */
    this.symmetric = null;
    /**
     * AEAD algorithm to encrypt the key with (if AEAD protection is enabled)
     * @type {enums.aead}
     */
    this.aead = null;
    /**
     * Whether the key is encrypted using the legacy AEAD format proposal from RFC4880bis
     * (i.e. it was encrypted with the flag `config.aeadProtect` in OpenPGP.js v5 or older).
     * This value is only relevant to know how to decrypt the key:
     * if AEAD is enabled, a v4 key is always re-encrypted using the standard AEAD mechanism.
     * @type {Boolean}
     * @private
     */
    this.isLegacyAEAD = null;
    /**
     * Decrypted private parameters, referenced by name
     * @type {Object}
     */
    this.privateParams = null;
    /**
     * `true` for keys whose integrity is already confirmed, based on
     * the AEAD encryption mechanism
     * @type {Boolean}
     * @private
     */
    this.usedModernAEAD = null;
  }

  // 5.5.3.  Secret-Key Packet Formats

  /**
   * Internal parser for private keys as specified in
   * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3}
   * @param {Uint8Array} bytes - Input string to read the packet from
   * @async
   */
  async read(bytes, config = defaultConfig) {
    // - A Public-Key or Public-Subkey packet, as described above.
    let i = await this.readPublicKey(bytes);
    const startOfSecretKeyData = i;

    // - One octet indicating string-to-key usage conventions.  Zero
    //   indicates that the secret-key data is not encrypted.  255 or 254
    //   indicates that a string-to-key specifier is being given.  Any
    //   other value is a symmetric-key encryption algorithm identifier.
    this.s2kUsage = bytes[i++];

    // - Only for a version 5 packet, a one-octet scalar octet count of
    //   the next 4 optional fields.
    if (this.version === 5) {
      i++;
    }

    // - Only for a version 6 packet where the secret key material is
    //   encrypted (that is, where the previous octet is not zero), a one-
    //   octet scalar octet count of the cumulative length of all the
    //   following optional string-to-key parameter fields.
    if (this.version === 6 && this.s2kUsage) {
      i++;
    }

    try {
      // - [Optional] If string-to-key usage octet was 255, 254, or 253, a
      //   one-octet symmetric encryption algorithm.
      if (this.s2kUsage === 255 || this.s2kUsage === 254 || this.s2kUsage === 253) {
        this.symmetric = bytes[i++];

        // - [Optional] If string-to-key usage octet was 253, a one-octet
        //   AEAD algorithm.
        if (this.s2kUsage === 253) {
          this.aead = bytes[i++];
        }

        // - [Optional] Only for a version 6 packet, and if string-to-key usage
        //   octet was 255, 254, or 253, an one-octet count of the following field.
        if (this.version === 6) {
          i++;
        }

        // - [Optional] If string-to-key usage octet was 255, 254, or 253, a
        //   string-to-key specifier.  The length of the string-to-key
        //   specifier is implied by its type, as described above.
        const s2kType = bytes[i++];
        this.s2k = newS2KFromType(s2kType);
        i += this.s2k.read(bytes.subarray(i, bytes.length));

        if (this.s2k.type === 'gnu-dummy') {
          return;
        }
      } else if (this.s2kUsage) {
        this.symmetric = this.s2kUsage;
      }


      if (this.s2kUsage) {
        // OpenPGP.js up to v5 used to support encrypting v4 keys using AEAD as specified by draft RFC4880bis (https://www.ietf.org/archive/id/draft-ietf-openpgp-rfc4880bis-10.html#section-5.5.3-3.5).
        // This legacy format is incompatible, but fundamentally indistinguishable, from that of the crypto-refresh for v4 keys (v5 keys are always in legacy format).
        // While parsing the key may succeed (if IV and AES block sizes match), key decryption will always
        // fail if the key was parsed according to the wrong format, since the keys are processed differently.
        // Thus, for v4 keys, we rely on the caller to instruct us to process the key as legacy, via config flag.
        this.isLegacyAEAD = this.s2kUsage === 253 && (
          this.version === 5 || (this.version === 4 && config.parseAEADEncryptedV4KeysAsLegacy));
        // - crypto-refresh: If string-to-key usage octet was 255, 254 [..], an initialization vector (IV)
        // of the same length as the cipher's block size.
        // - RFC4880bis (v5 keys, regardless of AEAD): an Initial Vector (IV) of the same length as the
        //   cipher's block size. If string-to-key usage octet was 253 the IV is used as the nonce for the AEAD algorithm.
        // If the AEAD algorithm requires a shorter nonce, the high-order bits of the IV are used and the remaining bits MUST be zero
        if (this.s2kUsage !== 253 || this.isLegacyAEAD) {
          this.iv = bytes.subarray(
            i,
            i + crypto.getCipherParams(this.symmetric).blockSize
          );
          this.usedModernAEAD = false;
        } else {
          // crypto-refresh: If string-to-key usage octet was 253 (that is, the secret data is AEAD-encrypted),
          // an initialization vector (IV) of size specified by the AEAD algorithm (see Section 5.13.2), which
          // is used as the nonce for the AEAD algorithm.
          this.iv = bytes.subarray(
            i,
            i + crypto.getAEADMode(this.aead).ivLength
          );
          // the non-legacy AEAD encryption mechanism also authenticates public key params; no need for manual validation.
          this.usedModernAEAD = true;
        }

        i += this.iv.length;
      }
    } catch (e) {
      // if the s2k is unsupported, we still want to support encrypting and verifying with the given key
      if (!this.s2kUsage) throw e; // always throw for decrypted keys
      this.unparseableKeyMaterial = bytes.subarray(startOfSecretKeyData);
      this.isEncrypted = true;
    }

    // - Only for a version 5 packet, a four-octet scalar octet count for
    //   the following key material.
    if (this.version === 5) {
      i += 4;
    }

    // - Plain or encrypted multiprecision integers comprising the secret
    //   key data.  These algorithm-specific fields are as described
    //   below.
    this.keyMaterial = bytes.subarray(i);
    this.isEncrypted = !!this.s2kUsage;

    if (!this.isEncrypted) {
      let cleartext;
      if (this.version === 6) {
        cleartext = this.keyMaterial;
      } else {
        cleartext = this.keyMaterial.subarray(0, -2);
        if (!util.equalsUint8Array(util.writeChecksum(cleartext), this.keyMaterial.subarray(-2))) {
          throw new Error('Key checksum mismatch');
        }
      }
      try {
        const { read, privateParams } = crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams);
        if (read < cleartext.length) {
          throw new Error('Error reading MPIs');
        }
        this.privateParams = privateParams;
      } catch (err) {
        if (err instanceof UnsupportedError) throw err;
        // avoid throwing potentially sensitive errors
        throw new Error('Error reading MPIs');
      }
    }
  }

  /**
   * Creates an OpenPGP key packet for the given key.
   * @returns {Uint8Array} A string of bytes containing the secret key OpenPGP packet.
   */
  write() {
    const serializedPublicKey = this.writePublicKey();
    if (this.unparseableKeyMaterial) {
      return util.concatUint8Array([
        serializedPublicKey,
        this.unparseableKeyMaterial
      ]);
    }

    const arr = [serializedPublicKey];
    arr.push(new Uint8Array([this.s2kUsage]));

    const optionalFieldsArr = [];
    // - [Optional] If string-to-key usage octet was 255, 254, or 253, a
    //   one- octet symmetric encryption algorithm.
    if (this.s2kUsage === 255 || this.s2kUsage === 254 || this.s2kUsage === 253) {
      optionalFieldsArr.push(this.symmetric);

      // - [Optional] If string-to-key usage octet was 253, a one-octet
      //   AEAD algorithm.
      if (this.s2kUsage === 253) {
        optionalFieldsArr.push(this.aead);
      }

      const s2k = this.s2k.write();

      // - [Optional] Only for a version 6 packet, and if string-to-key usage
      //   octet was 255, 254, or 253, an one-octet count of the following field.
      if (this.version === 6) {
        optionalFieldsArr.push(s2k.length);
      }

      // - [Optional] If string-to-key usage octet was 255, 254, or 253, a
      //   string-to-key specifier.  The length of the string-to-key
      //   specifier is implied by its type, as described above.
      optionalFieldsArr.push(...s2k);
    }

    // - [Optional] If secret data is encrypted (string-to-key usage octet
    //   not zero), an Initial Vector (IV) of the same length as the
    //   cipher's block size.
    if (this.s2kUsage && this.s2k.type !== 'gnu-dummy') {
      optionalFieldsArr.push(...this.iv);
    }

    if (this.version === 5 || (this.version === 6 && this.s2kUsage)) {
      arr.push(new Uint8Array([optionalFieldsArr.length]));
    }
    arr.push(new Uint8Array(optionalFieldsArr));

    if (!this.isDummy()) {
      if (!this.s2kUsage) {
        this.keyMaterial = crypto.serializeParams(this.algorithm, this.privateParams);
      }

      if (this.version === 5) {
        arr.push(util.writeNumber(this.keyMaterial.length, 4));
      }
      arr.push(this.keyMaterial);

      if (!this.s2kUsage && this.version !== 6) {
        arr.push(util.writeChecksum(this.keyMaterial));
      }
    }

    return util.concatUint8Array(arr);
  }

  /**
   * Check whether secret-key data is available in decrypted form.
   * Returns false for gnu-dummy keys and null for public keys.
   * @returns {Boolean|null}
   */
  isDecrypted() {
    return this.isEncrypted === false;
  }

  /**
   * Check whether the key includes secret key material.
   * Some secret keys do not include it, and can thus only be used
   * for public-key operations (encryption and verification).
   * Such keys are:
   * - GNU-dummy keys, where the secret material has been stripped away
   * - encrypted keys with unsupported S2K or cipher
   */
  isMissingSecretKeyMaterial() {
    return this.unparseableKeyMaterial !== undefined || this.isDummy();
  }

  /**
   * Check whether this is a gnu-dummy key
   * @returns {Boolean}
   */
  isDummy() {
    return !!(this.s2k && this.s2k.type === 'gnu-dummy');
  }

  /**
   * Remove private key material, converting the key to a dummy one.
   * The resulting key cannot be used for signing/decrypting but can still verify signatures.
   * @param {Object} [config] - Full configuration, defaults to openpgp.config
   */
  makeDummy(config = defaultConfig) {
    if (this.isDummy()) {
      return;
    }
    if (this.isDecrypted()) {
      this.clearPrivateParams();
    }
    delete this.unparseableKeyMaterial;
    this.isEncrypted = null;
    this.keyMaterial = null;
    this.s2k = newS2KFromType(enums.s2k.gnu, config);
    this.s2k.algorithm = 0;
    this.s2k.c = 0;
    this.s2k.type = 'gnu-dummy';
    this.s2kUsage = 254;
    this.symmetric = enums.symmetric.aes256;
    this.isLegacyAEAD = null;
    this.usedModernAEAD = null;
  }

  /**
   * Encrypt the payload. By default, we use aes256 and iterated, salted string
   * to key specifier. If the key is in a decrypted state (isEncrypted === false)
   * and the passphrase is empty or undefined, the key will be set as not encrypted.
   * This can be used to remove passphrase protection after calling decrypt().
   * @param {String} passphrase
   * @param {Object} [config] - Full configuration, defaults to openpgp.config
   * @throws {Error} if encryption was not successful
   * @async
   */
  async encrypt(passphrase, config = defaultConfig) {
    if (this.isDummy()) {
      return;
    }

    if (!this.isDecrypted()) {
      throw new Error('Key packet is already encrypted');
    }

    if (!passphrase) {
      throw new Error('A non-empty passphrase is required for key encryption.');
    }

    this.s2k = newS2KFromConfig(config);
    this.s2k.generateSalt();
    const cleartext = crypto.serializeParams(this.algorithm, this.privateParams);
    this.symmetric = enums.symmetric.aes256;

    const { blockSize } = crypto.getCipherParams(this.symmetric);

    if (config.aeadProtect) {
      this.s2kUsage = 253;
      this.aead = enums.aead.eax;
      const mode = crypto.getAEADMode(this.aead);
      this.isLegacyAEAD = this.version === 5; // v4 is always re-encrypted with standard format instead.
      this.usedModernAEAD = !this.isLegacyAEAD; // legacy AEAD does not guarantee integrity of public key material

      const serializedPacketTag = writeTag(this.constructor.tag);
      const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag);

      const modeInstance = await mode(this.symmetric, key);
      this.iv = this.isLegacyAEAD ? crypto.random.getRandomBytes(blockSize) : crypto.random.getRandomBytes(mode.ivLength);
      const associateData = this.isLegacyAEAD ?
        new Uint8Array() :
        util.concatUint8Array([serializedPacketTag, this.writePublicKey()]);

      this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), associateData);
    } else {
      this.s2kUsage = 254;
      this.usedModernAEAD = false;
      const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric);
      this.iv = crypto.random.getRandomBytes(blockSize);
      this.keyMaterial = await crypto.mode.cfb.encrypt(this.symmetric, key, util.concatUint8Array([
        cleartext,
        await crypto.hash.sha1(cleartext, config)
      ]), this.iv, config);
    }
  }

  /**
   * Decrypts the private key params which are needed to use the key.
   * Successful decryption does not imply key integrity, call validate() to confirm that.
   * {@link SecretKeyPacket.isDecrypted} should be false, as
   * otherwise calls to this function will throw an error.
   * @param {String} passphrase - The passphrase for this private key as string
   * @throws {Error} if the key is already decrypted, or if decryption was not successful
   * @async
   */
  async decrypt(passphrase) {
    if (this.isDummy()) {
      return false;
    }

    if (this.unparseableKeyMaterial) {
      throw new Error('Key packet cannot be decrypted: unsupported S2K or cipher algo');
    }

    if (this.isDecrypted()) {
      throw new Error('Key packet is already decrypted.');
    }

    let key;
    const serializedPacketTag = writeTag(this.constructor.tag); // relevant for AEAD only
    if (this.s2kUsage === 254 || this.s2kUsage === 253) {
      key = await produceEncryptionKey(
        this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag, this.isLegacyAEAD);
    } else if (this.s2kUsage === 255) {
      throw new Error('Encrypted private key is authenticated using an insecure two-byte hash');
    } else {
      throw new Error('Private key is encrypted using an insecure S2K function: unsalted MD5');
    }

    let cleartext;
    if (this.s2kUsage === 253) {
      const mode = crypto.getAEADMode(this.aead);
      const modeInstance = await mode(this.symmetric, key);
      try {
        const associateData = this.isLegacyAEAD ?
          new Uint8Array() :
          util.concatUint8Array([serializedPacketTag, this.writePublicKey()]);
        cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), associateData);
      } catch (err) {
        if (err.message === 'Authentication tag mismatch') {
          throw new Error('Incorrect key passphrase: ' + err.message);
        }
        throw err;
      }
    } else {
      const cleartextWithHash = await crypto.mode.cfb.decrypt(this.symmetric, key, this.keyMaterial, this.iv);

      cleartext = cleartextWithHash.subarray(0, -20);
      const hash = await crypto.hash.sha1(cleartext);

      if (!util.equalsUint8Array(hash, cleartextWithHash.subarray(-20))) {
        throw new Error('Incorrect key passphrase');
      }
    }

    try {
      const { privateParams } = crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams);
      this.privateParams = privateParams;
    } catch (err) {
      throw new Error('Error reading MPIs');
    }
    this.isEncrypted = false;
    this.keyMaterial = null;
    this.s2kUsage = 0;
    this.aead = null;
    this.symmetric = null;
    this.isLegacyAEAD = null;
  }

  /**
   * Checks that the key parameters are consistent
   * @throws {Error} if validation was not successful
   * @async
   */
  async validate() {
    if (this.isDummy()) {
      return;
    }

    if (!this.isDecrypted()) {
      throw new Error('Key is not decrypted');
    }

    if (this.usedModernAEAD) {
      // key integrity confirmed by successful AEAD decryption
      return;
    }

    let validParams;
    try {
      // this can throw if some parameters are undefined
      validParams = await crypto.validateParams(this.algorithm, this.publicParams, this.privateParams);
    } catch (_) {
      validParams = false;
    }
    if (!validParams) {
      throw new Error('Key is invalid');
    }
  }

  async generate(bits, curve) {
    // The deprecated OIDs for Ed25519Legacy and Curve25519Legacy are used in legacy version 4 keys and signatures.
    // Implementations MUST NOT accept or generate v6 key material using the deprecated OIDs.
    if (this.version === 6 && (
      (this.algorithm === enums.publicKey.ecdh && curve === enums.curve.curve25519Legacy) ||
      this.algorithm === enums.publicKey.eddsaLegacy
    )) {
      throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`);
    }
    const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve);
    this.privateParams = privateParams;
    this.publicParams = publicParams;
    this.isEncrypted = false;
  }

  /**
   * Clear private key parameters
   */
  clearPrivateParams() {
    if (this.isMissingSecretKeyMaterial()) {
      return;
    }

    Object.keys(this.privateParams).forEach(name => {
      const param = this.privateParams[name];
      param.fill(0);
      delete this.privateParams[name];
    });
    this.privateParams = null;
    this.isEncrypted = true;
  }
}

/**
 * Derive encryption key
 * @param {Number} keyVersion - key derivation differs for v5 keys
 * @param {module:type/s2k} s2k
 * @param {String} passphrase
 * @param {module:enums.symmetric} cipherAlgo
 * @param {module:enums.aead} [aeadMode] - for AEAD-encrypted keys only (excluding v5)
 * @param {Uint8Array} [serializedPacketTag] - for AEAD-encrypted keys only (excluding v5)
 * @param {Boolean} [isLegacyAEAD] - for AEAD-encrypted keys from RFC4880bis (v4 and v5 only)
 * @returns encryption key
 */
async function produceEncryptionKey(keyVersion, s2k, passphrase, cipherAlgo, aeadMode, serializedPacketTag, isLegacyAEAD) {
  const { keySize } = crypto.getCipherParams(cipherAlgo);
  const derivedKey = await s2k.produceKey(passphrase, keySize);
  if (!aeadMode || keyVersion === 5 || isLegacyAEAD) {
    return derivedKey;
  }
  const info = util.concatUint8Array([
    serializedPacketTag,
    new Uint8Array([keyVersion, cipherAlgo, aeadMode])
  ]);
  return computeHKDF(enums.hash.sha256, derivedKey, new Uint8Array(), info, keySize);
}

export default SecretKeyPacket;
