import * as miscreant from 'miscreant';
import { AbstractSymmetricCipher } from '../../base/interface';
import { concatByteArray, ValueError } from '../../utility';

/**
 * RFC5297 - https://tools.ietf.org/html/rfc5297
    Notes on AES-SIV, the primary encryption for use as of 2021.

    This mode of operation is relevant for us because it is the only one that provides the required security
    properties while also having adequate performance on all relevant platforms:
      - General security
        We need to have an encryption that provides not only confidentiality but authenticity as well.
      - Distributed security
        Our system uses online and offline message exchanges among multiple participants of groups in distributed
        systems. Basically there is no suitable solution to coordinate sequential nonce values and using random
        values is not a security guarantee because keys can be used for a long time or very excessively. This means
        we require high security guarantees if a nonce is randomly reused.
      - Performance
        Our clients will access the system with web browsers and we all know those have the worst performance in
        more ways than one. This means providing a good performance to the web platform is critically important.
        Luckily AES-SIV is a clever construction of AES-CBC and AES-CTR modes. Those two are among the very few
        supported modes by the WebCrypto standard. Properly utilizing the WebCrypto API, a SIV construct can reach
        reasonable performance.

    About the general performance limitations of AES-SIV.
        The drawback of the mode is that it is costlier by being a two-pass algorithm. We should still be better or
        at around the same performance as using a lighter algorithm with JS-only (maybe even WASM) implementation.
        XSalsa20 (or XChaCha20) was a candidate because it is the encryption used by NaCl, but it's performance was
        not satisfactory. It's nonce security is very comparable to being misuse resistant because of its size, but
        it does not actually guarantee security in case of a reuse.

    About the imperfect MAC-and/than-Encrypt construct.
        It's unfortunate. The good news is that the typical issues about MAC-and/then-Encrypt are timing related
        which can be and are handled well by the crypto library.
        Padding oracle attacks are not effective because the CTR is a stream cipher and there is no padding in the
        encrypted ciphertext. Authentication verification is implemented by hashing the tags before comparison, which
        makes the authentication tag resistant to timing based guessing as well. The performance issue of being an
        offline AES mode makes stream-manipulating attacks irrelevant.
        TODO: Check the same properties for the Javascript version as well. (MAC comparison mostly)

    Security considerations and safety limits for keys.
        Use 64B keys to utilize AES-256 with AES-SIV, as opposed to 32B keys for most other constructs. (using
            AES-GCM-SIV could alleviate this issue, but that is not available with good performance)
        No more than 2^48 invocations shall be made with the same key (approx. 200 trillion).
            - 2^48 is the limit of CMAC
            - 2^64 is the limit of SIV, but we need to limit to the CMAC
        The key use limits in terms of rotation times:
            | Key lifetime  | All uses / sec | P2P req-rep / sec | Group 1000 / sec | Group 100000 / sec
            +---------------+----------------+-------------------+------------------+--------------------
            | 100 Years     |  89.1 thousand |  44.59 thousand   |     89           |    0.89
            |   1 Year      |   8.9 million  |   4.45 million    |   8900           |   89
            |   6 Months    |  17   million  |   8.9  million    |  17800           |  178
            |   1 Month     | 107   million  |  53.5  million    | 107000           | 1070
            The takeaway from the table is that since key rotation can be done cheaply by HKDF, huge groups can be
            supported with ridiculous message frequency for the same key.

    The doubled key size specific to AES-SIV is not even that bad when it is stored and not derived. The current
    construct of 12B nonce will progressively return storage after each message compared to solutions that utilize
    a huge-nonce (24B) for security. Just after storing 3 encrypted messages we have a net positive.

    NOTE: This class is only here to group the notes to the AES-SIV configurations in general.
 */
export abstract class AbstractAESCipherSIV extends AbstractSymmetricCipher {
  /*public static abstract encrypt: Function;

    public static abstract decrypt(cipher: Uint8Array, key: Uint8Array): bytes;

    public static abstract describe(): string;*/
}

/**
 * Primary SIV cipher config with a medium sized nonce for extra security.
 */
export class AESCipherSIVNonce96Worker extends AbstractAESCipherSIV {
  public static encrypt(
    plain: Uint8Array,
    key: Uint8Array,
    nonce?: Uint8Array,
    assoc: Uint8Array = new Uint8Array([]),
    prefix: Uint8Array = new Uint8Array([])
  ): Promise<Uint8Array> {
    if (nonce == undefined) {
      nonce = self.crypto.getRandomValues(new Uint8Array(12));
    } else {
      if (nonce.byteLength != 12) {
        throw new ValueError('nonce length must be 12');
      }
    }

    return miscreant.SIV.importKey(key, 'AES-SIV', new miscreant.WebCryptoProvider(crypto)).then(
      (extractedKey) => {
        return extractedKey.seal(plain, [nonce]).then((encrypted) => {
          return concatByteArray(prefix, nonce, encrypted);
        });
      }
    );
  }

  public static decrypt(
    cipher: Uint8Array,
    key: Uint8Array,
    assoc: Uint8Array = new Uint8Array([])
  ): Promise<Uint8Array> {
    if (cipher.byteLength < 28) {
      throw new ValueError('cipher length must be at least 28');
    }

    return miscreant.SIV.importKey(key, 'AES-SIV', new miscreant.WebCryptoProvider(crypto)).then(
      (extractedKey) => {
        // the first 12 byte is the nonce
        return extractedKey.open(cipher.slice(12, cipher.byteLength), [cipher.slice(0, 12)]);
      }
    );
  }

  public static describe(): string {
    return 'AES-SIV-N96-128/256';
  }
}

/**
 * Deterministic SIV encryption by not using a nonce.
    RFC5297 Section 4:
    In case AES SIV is used for key wrapping it is not necessary to provide a nonce component.
 */
export class AESCipherSIVDeterministicWorker extends AbstractAESCipherSIV {
  public static encrypt(
    plain: Uint8Array,
    key: Uint8Array,
    assoc?: Uint8Array,
    prefix: Uint8Array = new Uint8Array([])
  ): Promise<Uint8Array> {
    return miscreant.SIV.importKey(key, 'AES-SIV', new miscreant.WebCryptoProvider(crypto)).then(
      (extractedKey) => {
        return extractedKey.seal(plain, []).then((seal) => {
          return concatByteArray(prefix, seal);
        });
      }
    );
  }

  public static decrypt(
    cipher: Uint8Array,
    key: Uint8Array,
    assoc?: Uint8Array
  ): Promise<Uint8Array> {
    if (cipher.byteLength < 16) {
      throw new ValueError('cipher length must be at least 16');
    }
    return miscreant.SIV.importKey(key, 'AES-SIV', new miscreant.WebCryptoProvider(crypto)).then(
      (extractedKey) => {
        return extractedKey.open(cipher, []);
      }
    );
  }

  public static describe(): string {
    return 'AES-SIV-N0-128/256';
  }
}

/**
 * One of the most widely used and generally good AES constructs.
    The security is prone to breaking by repeating nonce values if not coordinated by the participants.
 */
export class AESCipherGCMWorker extends AbstractSymmetricCipher {
  public static encrypt(
    plain: Uint8Array,
    key: Uint8Array,
    nonce?: Uint8Array,
    assoc?: Uint8Array,
    prefix?: Uint8Array
  ): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      // TODO update typescript and remove this. The older version return with PromiseLike

      if (nonce == undefined || nonce.byteLength != 12) {
        throw new ValueError('nonce length must be 12');
      }

      let algo: AesGcmParams;
      algo = {
        name: 'AES-GCM',
        iv: nonce,
      };
      /*return*/ crypto.subtle
        .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
        .then((cryptoKey) => {
          /*return*/ crypto.subtle.encrypt(algo, cryptoKey, plain).then((deriveBits) => {
            /*return*/ resolve(concatByteArray(nonce, new Uint8Array(deriveBits)));
          }, reject);
        }, reject);
    });
  }

  public static decrypt(
    cipher: Uint8Array,
    key: Uint8Array,
    assoc?: Uint8Array
  ): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      // TODO update typescript and remove this. The older version return with PromiseLike

      if (cipher.byteLength < 28) {
        return reject(new ValueError('cipher length must be at least 28'));
      }

      let encryptedData = cipher.subarray(12, cipher.byteLength);
      let nonce = cipher.subarray(0, 12);

      let algo: AesGcmParams;
      algo = {
        name: 'AES-GCM',
        iv: nonce,
      };
      return crypto.subtle
        .importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt'])
        .then((cryptoKey) => {
          /*return*/ crypto.subtle.decrypt(algo, cryptoKey, encryptedData).then((deriveBits) => {
            /*return*/ resolve(new Uint8Array(deriveBits));
          }, reject);
        }, reject);
    });
  }

  public static describe(): string {
    return 'AES-GCM-128/256';
  }
}

/**
 * The simplest yet sometimes adequate and usable AES mode.
    NOTE: DO NOT USE THIS, PADDING PROBLEMS +-16 btes at the end of the encrypted data
*/
export class AESCipherCBCWorker extends AbstractSymmetricCipher {
  public static encrypt(
    plain: Uint8Array,
    key: Uint8Array,
    iv?: Uint8Array,
    prefix: Uint8Array = new Uint8Array([])
  ): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      // TODO update typescript and remove this. The older version return with PromiseLike

      if (iv == undefined) {
        iv = crypto.getRandomValues(new Uint8Array(16));
      } else {
        if (iv.byteLength != 16) {
          return reject(new ValueError('nonce length must be 16'));
        }
      }

      let algo: AesCbcParams;
      algo = {
        name: 'AES-CBC',
        iv,
      };
      return crypto.subtle
        .importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt'])
        .then((cryptoKey) => {
          /*return*/ crypto.subtle.encrypt(algo, cryptoKey, plain).then((deriveBits) => {
            /*return*/ resolve(concatByteArray(prefix, iv, new Uint8Array(deriveBits)));
          }, reject);
        }, reject);
    });
  }

  public static decrypt(cipher: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      // TODO update typescript and remove this. The older version return with PromiseLike
      if (cipher.byteLength < 16) {
        return reject(new ValueError('cipher length must be at least 16'));
      }

      let encryptedData = cipher.subarray(16, cipher.byteLength);
      let iv = cipher.subarray(0, 16);

      let algo: AesGcmParams;
      algo = {
        name: 'AES-CBC',
        iv,
      };

      return crypto.subtle
        .importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt'])
        .then((cryptoKey) => {
          /*return*/ crypto.subtle.decrypt(algo, cryptoKey, encryptedData).then((deriveBits) => {
            /*return*/ resolve(new Uint8Array(deriveBits));
          }, reject);
        }, reject);
    });
  }

  public static describe(): string {
    return 'AES-CBC-128/256';
  }
}
