/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "ctypes", "resource://gre/modules/ctypes.jsm");

const FLAGS_NOT_SET = 0;

const wintypes = {
  BOOL: ctypes.bool,
  BYTE: ctypes.uint8_t,
  DWORD: ctypes.uint32_t,
  PBYTE: ctypes.unsigned_char.ptr,
  PCHAR: ctypes.char.ptr,
  PDWORD: ctypes.uint32_t.ptr,
  PVOID: ctypes.voidptr_t,
  WORD: ctypes.uint16_t,
}

function OSCrypto() {
  this._structs = {};
  this._functions = new Map();
  this._libs = new Map();
  this._structs.DATA_BLOB = new ctypes.StructType("DATA_BLOB",
                                                  [
                                                    {cbData: wintypes.DWORD},
                                                    {pbData: wintypes.PVOID}
                                                  ]);

  try {

    this._libs.set("crypt32", ctypes.open("Crypt32"));
    this._libs.set("kernel32", ctypes.open("Kernel32"));

    this._functions.set("CryptProtectData",
                        this._libs.get("crypt32").declare("CryptProtectData",
                                                          ctypes.winapi_abi,
                                                          wintypes.DWORD,
                                                          this._structs.DATA_BLOB.ptr,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.DWORD,
                                                          this._structs.DATA_BLOB.ptr));
    this._functions.set("CryptUnprotectData",
                        this._libs.get("crypt32").declare("CryptUnprotectData",
                                                          ctypes.winapi_abi,
                                                          wintypes.DWORD,
                                                          this._structs.DATA_BLOB.ptr,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.PVOID,
                                                          wintypes.DWORD,
                                                          this._structs.DATA_BLOB.ptr));
    this._functions.set("LocalFree",
                        this._libs.get("kernel32").declare("LocalFree",
                                                           ctypes.winapi_abi,
                                                           wintypes.DWORD,
                                                           wintypes.PVOID));
  } catch (ex) {
    Cu.reportError(ex);
    this.finalize();
    throw ex;
  }
}
OSCrypto.prototype = {
  /**
   * Convert an array containing only two bytes unsigned numbers to a string.
   * @param {number[]} arr - the array that needs to be converted.
   * @returns {string} the string representation of the array.
   */
  arrayToString(arr) {
    let str = "";
    for (let i = 0; i < arr.length; i++) {
      str += String.fromCharCode(arr[i]);
    }
    return str;
  },

  /**
   * Convert a string to an array.
   * @param {string} str - the string that needs to be converted.
   * @returns {number[]} the array representation of the string.
   */
  stringToArray(str) {
    let arr = [];
    for (let i = 0; i < str.length; i++) {
      arr.push(str.charCodeAt(i));
    }
    return arr;
  },

  /**
   * Calculate the hash value used by IE as the name of the registry value where login details are
   * stored.
   * @param {string} data - the string value that needs to be hashed.
   * @returns {string} the hash value of the string.
   */
  getIELoginHash(data) {
    // return the two-digit hexadecimal code for a byte
    function toHexString(charCode) {
      return ("00" + charCode.toString(16)).slice(-2);
    }

    // the data needs to be encoded in null terminated UTF-16
    data += "\0";
    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                    createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-16";
    // result is an out parameter,
    // result.value will contain the array length
    let result = {};
    // dataArray is an array of bytes
    let dataArray = converter.convertToByteArray(data, result);
    // calculation of SHA1 hash value
    let cryptoHash = Cc["@mozilla.org/security/hash;1"].
                     createInstance(Ci.nsICryptoHash);
    cryptoHash.init(cryptoHash.SHA1);
    cryptoHash.update(dataArray, dataArray.length);
    let hash = cryptoHash.finish(false);

    let tail = 0; // variable to calculate value for the last 2 bytes
    // convert to a character string in hexadecimal notation
    for (let c of hash) {
      tail += c.charCodeAt(0);
    }
    hash += String.fromCharCode(tail % 256);

    // convert the binary hash data to a hex string.
    let hashStr = Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
    return hashStr.toUpperCase();
  },

  /**
   * Decrypt a string using the windows CryptUnprotectData API.
   * @param {string} data - the encrypted string that needs to be decrypted.
   * @param {?string} entropy - the entropy value of the decryption (could be null). Its value must
   * be the same as the one used when the data was encrypted.
   * @returns {string} the decryption of the string.
   */
  decryptData(data, entropy = null) {
    let array = this.stringToArray(data);
    let decryptedData = "";
    let encryptedData = wintypes.BYTE.array(array.length)(array);
    let inData = new this._structs.DATA_BLOB(encryptedData.length, encryptedData);
    let outData = new this._structs.DATA_BLOB();
    let entropyParam;
    if (entropy) {
      let entropyArray = this.stringToArray(entropy);
      entropyArray.push(0);
      let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
      let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
                                                        entropyData);
      entropyParam = optionalEntropy.address();
    } else {
      entropyParam = null;
    }

    let status = this._functions.get("CryptUnprotectData")(inData.address(), null,
                                     entropyParam,
                                     null, null, FLAGS_NOT_SET,
                                     outData.address());
    if (status === 0) {
      throw new Error("decryptData failed: " + status);
    }

    // convert byte array to JS string.
    let len = outData.cbData;
    let decrypted = ctypes.cast(outData.pbData,
                                wintypes.BYTE.array(len).ptr).contents;
    for (let i = 0; i < decrypted.length; i++) {
      decryptedData += String.fromCharCode(decrypted[i]);
    }

    this._functions.get("LocalFree")(outData.pbData);
    return decryptedData;
 },

  /**
   * Encrypt a string using the windows CryptProtectData API.
   * @param {string} data - the string that is going to be encrypted.
   * @param {?string} entropy - the entropy value of the encryption (could be null). Its value must
   * be the same as the one that is going to be used for the decryption.
   * @returns {string} the encrypted string.
   */
  encryptData(data, entropy = null) {
    let encryptedData = "";
    let decryptedData = wintypes.BYTE.array(data.length)(this.stringToArray(data));

    let inData = new this._structs.DATA_BLOB(data.length, decryptedData);
    let outData = new this._structs.DATA_BLOB();
    let entropyParam;
    if (!entropy) {
      entropyParam = null;
    } else {
      let entropyArray = this.stringToArray(entropy);
      entropyArray.push(0);
      let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
      let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
                                                        entropyData);
      entropyParam = optionalEntropy.address();
    }

    let status = this._functions.get("CryptProtectData")(inData.address(), null,
                                                         entropyParam,
                                                         null, null, FLAGS_NOT_SET,
                                                         outData.address());
    if (status === 0) {
      throw new Error("encryptData failed: " + status);
    }

    // convert byte array to JS string.
    let len = outData.cbData;
    let encrypted = ctypes.cast(outData.pbData,
                                wintypes.BYTE.array(len).ptr).contents;
    encryptedData = this.arrayToString(encrypted);
    this._functions.get("LocalFree")(outData.pbData);
    return encryptedData;
  },

  /**
   * Must be invoked once after last use of any of the provided helpers.
   */
  finalize() {
    this._structs = {};
    this._functions.clear();
    for (let lib of this._libs.values()) {
      try {
        lib.close();
      } catch (ex) {
        Cu.reportError(ex);
      }
    }
    this._libs.clear();
  },
};
