/* 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/. */

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

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

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

function LoginManagerCrypto_SDR() {
  this.init();
}

LoginManagerCrypto_SDR.prototype = {

  classID : Components.ID("{dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}"),
  QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerCrypto]),

  __sdrSlot : null, // PKCS#11 slot being used by the SDR.
  get _sdrSlot() {
    if (!this.__sdrSlot) {
      let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"].
                    getService(Ci.nsIPKCS11ModuleDB);
      this.__sdrSlot = modules.findSlotByName("");
    }
    return this.__sdrSlot;
  },

  __decoderRing : null,  // nsSecretDecoderRing service
  get _decoderRing() {
    if (!this.__decoderRing)
      this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
                           getService(Ci.nsISecretDecoderRing);
    return this.__decoderRing;
  },

  __utfConverter : null, // UCS2 <--> UTF8 string conversion
  get _utfConverter() {
    if (!this.__utfConverter) {
      this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                            createInstance(Ci.nsIScriptableUnicodeConverter);
      this.__utfConverter.charset = "UTF-8";
    }
    return this.__utfConverter;
  },

  _utfConverterReset : function() {
    this.__utfConverter = null;
  },

  _uiBusy : false,


  init : function () {
    // Check to see if the internal PKCS#11 token has been initialized.
    // If not, set a blank password.
    let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
                  getService(Ci.nsIPK11TokenDB);

    let token = tokenDB.getInternalKeyToken();
    if (token.needsUserInit) {
      this.log("Initializing key3.db with default blank password.");
      token.initPassword("");
    }
  },


  /*
   * encrypt
   *
   * Encrypts the specified string, using the SecretDecoderRing.
   *
   * Returns the encrypted string, or throws an exception if there was a
   * problem.
   */
  encrypt : function (plainText) {
    let cipherText = null;

    let wasLoggedIn = this.isLoggedIn;
    let canceledMP = false;

    this._uiBusy = true;
    try {
      let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
      plainOctet += this._utfConverter.Finish();
      cipherText = this._decoderRing.encryptString(plainOctet);
    } catch (e) {
      this.log("Failed to encrypt string. (" + e.name + ")");
      // If the user clicks Cancel, we get NS_ERROR_FAILURE.
      // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
      if (e.result == Cr.NS_ERROR_FAILURE) {
        canceledMP = true;
        throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
      } else {
        throw Components.Exception("Couldn't encrypt string", Cr.NS_ERROR_FAILURE);
      }
    } finally {
      this._uiBusy = false;
      // If we triggered a master password prompt, notify observers.
      if (!wasLoggedIn && this.isLoggedIn)
        this._notifyObservers("passwordmgr-crypto-login");
      else if (canceledMP)
        this._notifyObservers("passwordmgr-crypto-loginCanceled");
    }
    return cipherText;
  },


  /*
   * decrypt
   *
   * Decrypts the specified string, using the SecretDecoderRing.
   *
   * Returns the decrypted string, or throws an exception if there was a
   * problem.
   */
  decrypt : function (cipherText) {
    let plainText = null;

    let wasLoggedIn = this.isLoggedIn;
    let canceledMP = false;

    this._uiBusy = true;
    try {
      let plainOctet;
      plainOctet = this._decoderRing.decryptString(cipherText);
      plainText = this._utfConverter.ConvertToUnicode(plainOctet);
    } catch (e) {
      this.log("Failed to decrypt string: " + cipherText +
          " (" + e.name + ")");

      // In the unlikely event the converter threw, reset it.
      this._utfConverterReset();

      // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
      // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
      // Wrong passwords are handled by the decoderRing reprompting;
      // we get no notification.
      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
        canceledMP = true;
        throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
      } else {
        throw Components.Exception("Couldn't decrypt string", Cr.NS_ERROR_FAILURE);
      }
    } finally {
      this._uiBusy = false;
      // If we triggered a master password prompt, notify observers.
      if (!wasLoggedIn && this.isLoggedIn)
        this._notifyObservers("passwordmgr-crypto-login");
      else if (canceledMP)
        this._notifyObservers("passwordmgr-crypto-loginCanceled");
    }

    return plainText;
  },


  /*
   * uiBusy
   */
  get uiBusy() {
    return this._uiBusy;
  },


  /*
   * isLoggedIn
   */
  get isLoggedIn() {
    let status = this._sdrSlot.status;
    this.log("SDR slot status is " + status);
    if (status == Ci.nsIPKCS11Slot.SLOT_READY ||
        status == Ci.nsIPKCS11Slot.SLOT_LOGGED_IN)
      return true;
    if (status == Ci.nsIPKCS11Slot.SLOT_NOT_LOGGED_IN)
      return false;
    throw Components.Exception("unexpected slot status: " + status, Cr.NS_ERROR_FAILURE);
  },


  /*
   * defaultEncType
   */
  get defaultEncType() {
    return Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
  },


  /*
   * _notifyObservers
   */
  _notifyObservers : function(topic) {
    this.log("Prompted for a master password, notifying for " + topic);
    Services.obs.notifyObservers(null, topic, null);
  },
}; // end of nsLoginManagerCrypto_SDR implementation

XPCOMUtils.defineLazyGetter(this.LoginManagerCrypto_SDR.prototype, "log", () => {
  let logger = LoginHelper.createLogger("Login crypto");
  return logger.log.bind(logger);
});

var component = [LoginManagerCrypto_SDR];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
