services/mobileid/MobileIdentityManager.jsm
author Mike Hommey <mh+mozilla@glandium.org>
Sat, 04 Oct 2014 10:33:00 +0900
changeset 208801 d959a6081ceacde13d41b8a4ee192c912c85ef02
parent 206761 5f470ac9f032d7eeb8c01d016fe1cc5b987813cd
child 208956 2f99196c505e869be276b1488044c8d12751519a
permissions -rw-r--r--
Bug 1077151 - Always use expandlibs descriptors when they exist. r=mshal Currently, when there is both an expandlibs descriptor and an actual static library, expandlibs picks the static library. This has the side effect that if there are object files in the static library that aren't directly used, they're dropped when linking, even when they export symbols that would be exported in the final linked binary. In most cases in the code base, files are not dropped that way. The most notable counter-example is xpcomglue, where actually not dropping files leads to link failure because of missing symbols those files reference (yes, that would tend to say the glue is broken in some way). On the opposite side, there is mozglue, which does have both a descriptor and a static library (the latter being necessary for the SDK), and that linking as a static library drops files that shouldn't be dropped (like jemalloc). We're currently relying on -Wl,--whole-archive for those files not to be dropped, but that won't really be possible without much hassle in a world where mozglue dependencies live in moz.build land. Switching expandlibs to use descriptors when they exist, even when there is a static library (so, the opposite of the current behavior) allows to drop -Wl,--whole-archive and prepare for a better future. However, as mentioned, xpcomglue does still require to be linked through the static library, so we need to make it a static library only. To achieve that, we make NO_EXPAND_LIBS now actually mean no expandlibs and use that to build the various different xpcomglues.

/* 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";

this.EXPORTED_SYMBOLS = ["MobileIdentityManager"];

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

Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

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

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

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

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

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

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

XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                   "@mozilla.org/parentprocessmessagemanager;1",
                                   "nsIMessageListenerManager");

XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
                                   "@mozilla.org/permissionmanager;1",
                                   "nsIPermissionManager");

XPCOMUtils.defineLazyServiceGetter(this, "securityManager",
                                   "@mozilla.org/scriptsecuritymanager;1",
                                   "nsIScriptSecurityManager");

XPCOMUtils.defineLazyServiceGetter(this, "appsService",
                                   "@mozilla.org/AppsService;1",
                                   "nsIAppsService");

#ifdef MOZ_B2G_RIL
XPCOMUtils.defineLazyServiceGetter(this, "Ril",
                                   "@mozilla.org/ril;1",
                                   "nsIRadioInterfaceLayer");

XPCOMUtils.defineLazyServiceGetter(this, "IccProvider",
                                   "@mozilla.org/ril/content-helper;1",
                                   "nsIIccProvider");

XPCOMUtils.defineLazyServiceGetter(this, "MobileConnectionService",
                                   "@mozilla.org/mobileconnection/mobileconnectionservice;1",
                                   "nsIMobileConnectionService");
#endif


this.MobileIdentityManager = {

  init: function() {
    log.debug("MobileIdentityManager init");
    Services.obs.addObserver(this, "xpcom-shutdown", false);
    ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
    this.messageManagers = {};
    this.keyPairs = {};
    this.certificates = {};
  },

  receiveMessage: function(aMessage) {
    log.debug("Received " + aMessage.name);

    if (aMessage.name !== GET_ASSERTION_IPC_MSG) {
      return;
    }

    let msg = aMessage.json;

    // We save the message target message manager so we can later dispatch
    // back messages without broadcasting to all child processes.
    let promiseId = msg.promiseId;
    this.messageManagers[promiseId] = aMessage.target;

    this.getMobileIdAssertion(aMessage.principal, promiseId, msg.options);
  },

  observe: function(subject, topic, data) {
    if (topic != "xpcom-shutdown") {
      return;
    }

    ppmm.removeMessageListener(GET_ASSERTION_IPC_MSG, this);
    Services.obs.removeObserver(this, "xpcom-shutdown");
    this.messageManagers = null;
  },

  /*********************************************************
   * Getters
   ********************************************************/
#ifdef MOZ_B2G_RIL
  // We have these getters to allow mocking RIL stuff from the tests.
  get ril() {
    if (this._ril) {
      return this._ril;
    }
    return Ril;
  },

  get iccProvider() {
    if (this._iccProvider) {
      return this._iccProvider;
    }
    return IccProvider;
  },

  get mobileConnectionService() {
    if (this._mobileConnectionService) {
      return this._mobileConnectionService;
    }
    return MobileConnectionService;
  },
#endif

  get iccInfo() {
    if (this._iccInfo) {
      return this._iccInfo;
    }
#ifdef MOZ_B2G_RIL
    let self = this;
    let iccListener = {
      notifyStkCommand: function() {},

      notifyStkSessionEnd: function() {},

      notifyCardStateChanged: function() {},

      notifyIccInfoChanged: function() {
        // If we receive a notification about an ICC info change, we clear
        // the ICC related caches so they can be rebuilt with the new changes.

        log.debug("ICC info changed observed. Clearing caches");

        // We don't need to keep listening for changes until we rebuild the
        // cache again.
        for (let i = 0; i < self._iccInfo.length; i++) {
          self.iccProvider.unregisterIccMsg(self._iccInfo[i].clientId,
                                            iccListener);
        }

        self._iccInfo = null;
        self._iccIds = null;
      }
    };

    // _iccInfo is a local cache containing the information about the SIM cards
    // that it is interesting for the Mobile ID flow.
    // The index of this array does not necesarily need to match the real
    // identifier of the SIM card ("clientId" or "serviceId" in RIL language).
    this._iccInfo = [];

    for (let i = 0; i < this.ril.numRadioInterfaces; i++) {
      let rilContext = this.ril.getRadioInterface(i).rilContext;
      if (!rilContext) {
        log.warn("Tried to get the RIL context for an invalid service ID " + i);
        continue;
      }

      let info = rilContext.iccInfo;
      if (!info) {
        log.warn("No ICC info");
        continue;
      }

      let connection = this.mobileConnectionService.getItemByServiceId(i);
      let voice = connection && connection.voice;
      let data = connection && connection.data;
      let operator = null;
      if (voice &&
          voice.network &&
          voice.network.shortName &&
          voice.network.shortName.length) {
        operator = voice.network.shortName;
      } else if (data &&
                 data.network &&
                 data.network.shortName &&
                 data.network.shortName.length) {
        operator = data.network.shortName;
      }

      this._iccInfo.push({
        // Because it is possible that the _iccInfo array index doesn't match
        // the real client ID, we need to store this value for later usage.
        clientId: i,
        iccId: info.iccid,
        mcc: info.mcc,
        mnc: info.mnc,
        // GSM SIMs may have MSISDN while CDMA SIMs may have MDN
        msisdn: info.msisdn || info.mdn || null,
        operator: operator,
        roaming: voice && voice.roaming
      });

      // We need to subscribe to ICC change notifications so we can refresh
      // the cache if any change is observed.
      this.iccProvider.registerIccMsg(i, iccListener);
    }

    return this._iccInfo;
#endif
    return null;
  },

  get iccIds() {
#ifdef MOZ_B2G_RIL
    if (this._iccIds) {
      return this._iccIds;
    }

    this._iccIds = [];
    if (!this.iccInfo) {
      return this._iccIds;
    }

    for (let i = 0; i < this.iccInfo.length; i++) {
      this._iccIds.push(this.iccInfo[i].iccId);
    }

    return this._iccIds;
#endif
    return null;
  },

  get credStore() {
    if (!this._credStore) {
      this._credStore = new MobileIdentityCredentialsStore();
      this._credStore.init();
    }
    return this._credStore;
  },

  get ui() {
    if (!this._ui) {
      this._ui = Cc["@mozilla.org/services/mobileid-ui-glue;1"]
                   .createInstance(Ci.nsIMobileIdentityUIGlue);
      this._ui.oncancel = this.onUICancel.bind(this);
      this._ui.onresendcode = this.onUIResendCode.bind(this);
    }
    return this._ui;
  },

  get client() {
    if (!this._client) {
      this._client = new MobileIdentityClient();
    }
    return this._client;
  },

  get isMultiSim() {
    return this.iccInfo && this.iccInfo.length > 1;
  },

  getVerificationOptionsForIcc: function(aServiceId) {
    log.debug("getVerificationOptionsForIcc " + aServiceId);
    log.debug("iccInfo ${}", this.iccInfo[aServiceId]);
    // First of all we need to check if we already have existing credentials
    // for the given SIM information (ICC ID or MSISDN). If we have no valid
    // credentials, we have to check with the server which options do we have
    // to verify the associated phone number.
    return this.credStore.getByIccId(this.iccInfo[aServiceId].iccId)
    .then(
      (creds) => {
        if (creds) {
          this.iccInfo[aServiceId].credentials = creds;
          return;
        }
        return this.credStore.getByMsisdn(this.iccInfo[aServiceId].msisdn);
      }
    )
    .then(
      (creds) => {
        if (creds) {
          this.iccInfo[aServiceId].credentials = creds;
          return;
        }
        // We have no credentials for this SIM, so we need to ask the server
        // which options do we have to verify the phone number.
        // But we need to be online...
        if (Services.io.offline) {
          return Promise.reject(ERROR_OFFLINE);
        }
        return this.client.discover(this.iccInfo[aServiceId].msisdn,
                                    this.iccInfo[aServiceId].mcc,
                                    this.iccInfo[aServiceId].mnc,
                                    this.iccInfo[aServiceId].roaming);
      }
    )
    .then(
      (result) => {
        log.debug("Discover result ${}", result);
        if (!result || !result.verificationMethods) {
          return;
        }
        this.iccInfo[aServiceId].verificationMethods = result.verificationMethods;
        this.iccInfo[aServiceId].verificationDetails = result.verificationDetails;
        this.iccInfo[aServiceId].canDoSilentVerification =
          (result.verificationMethods.indexOf(SMS_MO_MT) != -1);
        return;
      }
    );
  },

  getVerificationOptions: function() {
    log.debug("getVerificationOptions");
    // We try to get if we already have credentials for any of the inserted
    // SIM cards if any is available and we try to get the possible
    // verification mechanisms for these SIM cards.
    // All this information will be stored in iccInfo.
    if (!this.iccInfo || !this.iccInfo.length) {
      let deferred = Promise.defer();
      deferred.resolve(null);
      return deferred.promise;
    }

    let promises = [];
    for (let i = 0; i < this.iccInfo.length; i++) {
      promises.push(this.getVerificationOptionsForIcc(i));
    }
    return Promise.all(promises);
  },

  getKeyPair: function(aSessionToken) {
    if (this.keyPairs[aSessionToken] &&
        this.keyPairs[aSessionToken].validUntil > this.client.hawk.now()) {
      return Promise.resolve(this.keyPairs[aSessionToken].keyPair);
    }

    let validUntil = this.client.hawk.now() + KEY_LIFETIME;
    let deferred = Promise.defer();
    jwcrypto.generateKeyPair("DS160", (error, kp) => {
      if (error) {
        return deferred.reject(error);
      }
      this.keyPairs[aSessionToken] = {
        keyPair: kp,
        validUntil: validUntil
      };
      delete this.certificates[aSessionToken];
      deferred.resolve(kp);
    });

    return deferred.promise;
  },

  getCertificate: function(aSessionToken, aPublicKey) {
    log.debug("getCertificate");
    if (this.certificates[aSessionToken] &&
        this.certificates[aSessionToken].validUntil > this.client.hawk.now()) {
      return Promise.resolve(this.certificates[aSessionToken].cert);
    }

    if (Services.io.offline) {
      return Promise.reject(ERROR_OFFLINE);
    }

    let validUntil = this.client.hawk.now() + KEY_LIFETIME;
    let deferred = Promise.defer();
    this.client.sign(aSessionToken, CERTIFICATE_LIFETIME,
                     aPublicKey)
    .then(
      (signedCert) => {
        log.debug("Got signed certificate");
        this.certificates[aSessionToken] = {
          cert: signedCert.cert,
          validUntil: validUntil
        };
        deferred.resolve(signedCert.cert);
      },
      deferred.reject
    );
    return deferred.promise;
  },

  /*********************************************************
   * Setters (for test only purposes)
   ********************************************************/
  set ui(aUi) {
    this._ui = aUi;
  },

  set credStore(aCredStore) {
    this._credStore = aCredStore;
  },

  set client(aClient) {
    this._client = aClient;
  },

  set iccInfo(aIccInfo) {
    this._iccInfo = aIccInfo;
  },

  /*********************************************************
   * UI callbacks
   ********************************************************/

  onUICancel: function() {
    log.debug("UI cancel");
    if (this.activeVerificationFlow) {
      this.activeVerificationFlow.cleanup(true);
    }
  },

  onUIResendCode: function() {
    log.debug("UI resend code");
    if (!this.activeVerificationFlow) {
      return;
    }
    this.doVerification();
  },

  /*********************************************************
   * Result helpers
   *********************************************************/
  success: function(aPromiseId, aResult) {
    let mm = this.messageManagers[aPromiseId];
    mm.sendAsyncMessage("MobileId:GetAssertion:Return:OK", {
      promiseId: aPromiseId,
      result: aResult
    });
  },

  error: function(aPromiseId, aError) {
    let mm = this.messageManagers[aPromiseId];
    mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
      promiseId: aPromiseId,
      error: aError
    });
  },

  /*********************************************************
   * Permissions helper
   ********************************************************/

  addPermission: function(aPrincipal) {
    permissionManager.addFromPrincipal(aPrincipal, MOBILEID_PERM,
                                       Ci.nsIPermissionManager.ALLOW_ACTION);
  },

  /*********************************************************
   * Phone number verification
   ********************************************************/

  rejectVerification: function(aReason) {
    if (!this.activeVerificationDeferred) {
      return;
    }
    this.activeVerificationDeferred.reject(aReason);
    this.activeVerificationDeferred = null;
    this.cleanupVerification(true);
  },

  resolveVerification: function(aResult) {
    if (!this.activeVerificationDeferred) {
      return;
    }
    this.activeVerificationDeferred.resolve(aResult);
    this.activeVerificationDeferred = null;
    this.cleanupVerification();
  },

  cleanupVerification: function() {
    if (!this.activeVerificationFlow) {
      return;
    }
    this.activeVerificationFlow.cleanup();
    this.activeVerificationFlow = null;
  },

  doVerification: function() {
    this.activeVerificationFlow.doVerification()
    .then(
      (verificationResult) => {
        log.debug("onVerificationResult ");
        if (!verificationResult || !verificationResult.sessionToken ||
            !verificationResult.msisdn) {
          return this.rejectVerification(
            ERROR_INTERNAL_INVALID_VERIFICATION_RESULT
          );
        }
        this.resolveVerification(verificationResult);
      }
    )
    .then(
      null,
      reason => {
        // Verification timeout.
        log.warn("doVerification " + reason);
      }
    );
  },

  _verificationFlow: function(aToVerify, aOrigin) {
    log.debug("toVerify ${}", aToVerify);

    // We create the corresponding verification flow and save its instance
    // in case that we need to cancel it or retrigger it because the user
    // requested its cancelation or a resend of the verification code.
    if (aToVerify.verificationMethod.indexOf(SMS_MT) != -1 &&
        aToVerify.msisdn &&
        aToVerify.verificationDetails &&
        aToVerify.verificationDetails.mtSender) {
      this.activeVerificationFlow = new MobileIdentitySmsMtVerificationFlow({
          origin: aOrigin,
          msisdn: aToVerify.msisdn,
          mcc: aToVerify.mcc,
          mnc: aToVerify.mnc,
          iccId: aToVerify.iccId,
          external: aToVerify.serviceId === undefined,
          mtSender: aToVerify.verificationDetails.mtSender
        },
        this.ui,
        this.client
      );
#ifdef MOZ_B2G_RIL
    } else if (aToVerify.verificationMethod.indexOf(SMS_MO_MT) != -1 &&
               aToVerify.serviceId &&
               aToVerify.verificationDetails &&
               aToVerify.verificationDetails.moVerifier &&
               aToVerify.verificationDetails.mtSender) {
      this.activeVerificationFlow = new MobileIdentitySmsMoMtVerificationFlow({
          origin: aOrigin,
          serviceId: aToVerify.serviceId,
          iccId: aToVerify.iccId,
          mtSender: aToVerify.verificationDetails.mtSender,
          moVerifier: aToVerify.verificationDetails.moVerifier
        },
        this.ui,
        this.client
      );
#endif
    } else {
      return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
    }

    if (!this.activeVerificationFlow) {
      return Promise.reject(ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW);
    }

    this.activeVerificationDeferred = Promise.defer();
    this.doVerification();
    return this.activeVerificationDeferred.promise;
  },

  verificationFlow: function(aUserSelection, aOrigin) {
    log.debug("verificationFlow ${}", aUserSelection);

    if (!aUserSelection) {
      return Promise.reject(ERROR_INTERNAL_INVALID_USER_SELECTION);
    }

    let serviceId = aUserSelection.serviceId || undefined;
    // We check if the user entered phone number corresponds with any of the
    // inserted SIMs known phone numbers.
    if (aUserSelection.msisdn && this.iccInfo) {
      for (let i = 0; i < this.iccInfo.length; i++) {
        if (aUserSelection.msisdn == this.iccInfo[i].msisdn) {
          serviceId = i;
          break;
        }
      }
    }

    let toVerify = {};

    if (serviceId !== undefined) {
      log.debug("iccInfo ${}", this.iccInfo[serviceId]);
      toVerify.serviceId = serviceId;
      toVerify.iccId = this.iccInfo[serviceId].iccId;
      toVerify.msisdn = this.iccInfo[serviceId].msisdn;
      toVerify.mcc = this.iccInfo[serviceId].mcc;
      toVerify.mnc = this.iccInfo[serviceId].mnc;
      toVerify.verificationMethod =
        this.iccInfo[serviceId].verificationMethods[0];
      toVerify.verificationDetails =
        this.iccInfo[serviceId].verificationDetails[toVerify.verificationMethod];
      return this._verificationFlow(toVerify, aOrigin);
    } else {
      toVerify.msisdn = aUserSelection.msisdn;
      toVerify.mcc = aUserSelection.mcc;
      return this.client.discover(aUserSelection.msisdn,
                                  aUserSelection.mcc)
      .then(
        (discoverResult) => {
          if (!discoverResult || !discoverResult.verificationMethods) {
            return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
          }
          log.debug("discoverResult ${}", discoverResult);
          toVerify.verificationMethod = discoverResult.verificationMethods[0];
          toVerify.verificationDetails =
            discoverResult.verificationDetails[toVerify.verificationMethod];
          return this._verificationFlow(toVerify, aOrigin);
        }
      );
    }
  },


  /*********************************************************
   * UI prompt functions.
   ********************************************************/

  // The phone number prompt will be used to confirm that the user wants to
  // verify and share a known phone number and to allow her to introduce an
  // external phone or to select between phone numbers or SIM cards (if the
  // phones are not known) in a multi-SIM scenario.
  // This prompt will be considered as the permission prompt and its choice
  // will be remembered per origin by default.
  prompt: function prompt(aPrincipal, aManifestURL, aPhoneInfo) {
    log.debug("prompt ${principal} ${manifest} ${phoneInfo}", {
      principal: aPrincipal,
      manifest: aManifestURL,
      phoneInfo: aPhoneInfo
    });

    let phoneInfoArray = [];

    if (aPhoneInfo) {
      phoneInfoArray.push(aPhoneInfo);
    }

    if (this.iccInfo) {
      for (let i = 0; i < this.iccInfo.length; i++) {
        // If we don't know the msisdn, there is no previous credentials and
        // a silent verification is not possible or if the msisdn is the one
        // that is already chosen, we don't list this SIM as an option.
        if ((!this.iccInfo[i].msisdn && !this.iccInfo[i].credentials &&
            !this.iccInfo[i].canDoSilentVerification) ||
            ((aPhoneInfo) &&
             (this.iccInfo[i].msisdn == aPhoneInfo.msisdn ||
              this.iccInfo[i].iccId == aPhoneInfo.iccId))) {
          continue;
        }

        let phoneInfo = new MobileIdentityUIGluePhoneInfo(
          this.iccInfo[i].msisdn,
          this.iccInfo[i].operator,
          i,                      // service ID
          this.iccInfo[i].iccId,  // iccId
          false                   // primary
        );
        phoneInfoArray.push(phoneInfo);
      }
    }

    return this.ui.startFlow(aManifestURL, phoneInfoArray)
    .then(
      (result) => {
        log.debug("startFlow result ${} ", result);
        if (!result ||
            (!result.phoneNumber && (result.serviceId === undefined))) {
          return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
        }

        let msisdn;
        let mcc;

        // If the user selected one of the existing SIM cards we have to check
        // that we either have the MSISDN for that SIM or we can do a silent
        // verification that does not require us to have the MSISDN in advance.
        // result.serviceId can be "0".
        if (result.serviceId !== undefined &&
            result.serviceId !== null) {
          let icc = this.iccInfo[result.serviceId];
          log.debug("icc ${}", icc);
          if (!icc || !icc.msisdn && !icc.canDoSilentVerification) {
            return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
          }
          msisdn = icc.msisdn;
          mcc = icc.mcc;
        } else {
          msisdn = result.prefix ? result.prefix + result.phoneNumber
                                 : result.phoneNumber;
          mcc = result.mcc;
        }

        // We need to check that the selected phone number is valid and
        // if it is not notify the UI about the error and allow the user to
        // retry.
        if (msisdn && mcc &&
            !PhoneNumberUtils.parseWithMCC(msisdn, mcc)) {
          this.ui.error(ERROR_INVALID_PHONE_NUMBER);
          return this.prompt(aPrincipal, aManifestURL, aPhoneInfo);
        }

        log.debug("Selected msisdn (if any): " + msisdn + " - " + mcc);

        // The user gave permission for the requester origin, so we store it.
        this.addPermission(aPrincipal);

        return {
          msisdn: msisdn,
          mcc: mcc,
          serviceId: result.serviceId
        };
      }
    );
  },

  promptAndVerify: function(aPrincipal, aManifestURL, aCreds) {
    log.debug("promptAndVerify " + aPrincipal + ", " + aManifestURL +
              ", ${}", aCreds);
    let userSelection;

    if (Services.io.offline) {
      return Promise.reject(ERROR_OFFLINE);
    }

    // Before prompting the user we need to check with the server the
    // phone number verification methods that are possible with the
    // SIMs inserted in the device.
    return this.getVerificationOptions()
    .then(
      () => {
        // If we have an exisiting credentials, we add its associated
        // phone number information to the list of choices to present
        // to the user within the selection prompt.
        let phoneInfo;
        if (aCreds) {
          phoneInfo = new MobileIdentityUIGluePhoneInfo(
            aCreds.msisdn,
            null,           // operator
            undefined,      // service ID
            aCreds.iccId,   // iccId
            true            // primary
          );
        }
        return this.prompt(aPrincipal, aManifestURL, phoneInfo);
      }
    )
    .then(
      (promptResult) => {
        log.debug("promptResult ${}", promptResult);
        // If we had credentials and the user didn't change her
        // selection we return them. Otherwise, we need to verify
        // the new number.
        if (promptResult.msisdn && aCreds &&
            promptResult.msisdn == aCreds.msisdn) {
          return aCreds;
        }

        // We might already have credentials for the user selected icc. In
        // that case, we update the credentials store with the new origin and
        // return the credentials.
        if (promptResult.serviceId) {
          let creds = this.iccInfo[promptResult.serviceId].credentials;
          if (creds) {
            this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
                               creds.sessionToken, this.iccIds);
            return creds;
          }
        }

        // Or we might already have credentials for the selected phone
        // number and so we do the same: update the credentials store with the
        // new origin and return the credentials.
        return this.credStore.getByMsisdn(promptResult.msisdn)
        .then(
          (creds) => {
            if (creds) {
              this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
                                 creds.sessionToken, this.iccIds);
              return creds;
            }
            // Otherwise, we need to verify the new number selected by the
            // user.
            return this.verificationFlow(promptResult, aPrincipal.origin);
          }
        );
      }
    );
  },

  /*********************************************************
   * Credentials check
   *********************************************************/

  checkNewCredentials: function(aOldCreds, aNewCreds, aOrigin) {
    // If there were previous credentials and the user changed her
    // choice, we need to remove the origin from the old credentials.
    if (aNewCreds.msisdn != aOldCreds.msisdn) {
      return this.credStore.removeOrigin(aOldCreds.msisdn,
                                         aOrigin)
      .then(
        () => {
          return aNewCreds;
        }
      );
    } else {
      // Otherwise, we update the status of the SIM cards in the device
      // so we know that the user decided not to take the chance to change
      // her selection. We won't bother her again until a new SIM card
      // change is detected.
      return this.credStore.setDeviceIccIds(aOldCreds.msisdn, this.iccIds)
      .then(
        () => {
          return aOldCreds;
        }
      );
    }
  },

  /*********************************************************
   * Assertion generation
   ********************************************************/

  generateAssertion: function(aCredentials, aOrigin) {
    if (!aCredentials.sessionToken) {
      return Promise.reject(ERROR_INTERNAL_INVALID_TOKEN);
    }

    let deferred = Promise.defer();

    this.getKeyPair(aCredentials.sessionToken)
    .then(
      (keyPair) => {
        log.debug("keyPair " + keyPair.serializedPublicKey);
        let options = {
          duration: ASSERTION_LIFETIME,
          now: this.client.hawk.now(),
          localtimeOffsetMsec: this.client.hawk.localtimeOffsetMsec
        };

        this.getCertificate(aCredentials.sessionToken,
                            keyPair.serializedPublicKey)
        .then(
          (signedCert) => {
            log.debug("generateAssertion " + signedCert);
            jwcrypto.generateAssertion(signedCert, keyPair,
                                       aOrigin, options,
                                       (error, assertion) => {
              if (error) {
                log.error("Error generating assertion " + err);
                deferred.reject(error);
                return;
              }
              this.credStore.add(aCredentials.iccId,
                                 aCredentials.msisdn,
                                 aOrigin,
                                 aCredentials.sessionToken,
                                 this.iccIds)
              .then(
                () => {
                  deferred.resolve(assertion);
                }
              );
            });
          }, deferred.reject
        );
      }
    );

    return deferred.promise;
  },

  getMobileIdAssertion: function(aPrincipal, aPromiseId, aOptions) {
    log.debug("getMobileIdAssertion ${}", aPrincipal);

    let uri = Services.io.newURI(aPrincipal.origin, null, null);
    let principal = securityManager.getAppCodebasePrincipal(
      uri, aPrincipal.appId, aPrincipal.isInBrowserElement);
    let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);

    let permission = permissionManager.testPermissionFromPrincipal(
      principal,
      MOBILEID_PERM
    );

    if (permission == Ci.nsIPermissionManager.DENY_ACTION ||
        permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
      this.error(aPromiseId, ERROR_PERMISSION_DENIED);
      return;
    }

    let _creds;

    // First of all we look if we already have credentials for this origin.
    // If we don't have credentials it means that it is the first time that
    // the caller requested an assertion.
    this.credStore.getByOrigin(aPrincipal.origin)
    .then(
      (creds) => {
        log.debug("creds ${creds} - ${origin}", { creds: creds,
                                                  origin: aPrincipal.origin });
        if (!creds || !creds.sessionToken) {
          log.debug("No credentials");
          return;
        }

        _creds = creds;

        // Even if we already have credentials for this origin, the consumer
        // of the API might want to force the identity selection dialog.
        if (aOptions.forceSelection || aOptions.refreshCredentials) {
          return this.promptAndVerify(principal, manifestURL, creds)
          .then(
            (newCreds) => {
              return this.checkNewCredentials(creds, newCreds,
                                              principal.origin);
            }
          );
        }

        // SIM change scenario.

        // It is possible that the SIM cards inserted in the device at the
        // moment of the previous verification where we obtained the credentials
        // has changed. In that case, we need to let the user knowabout this
        // situation. Otherwise, we just return the credentials.
        log.debug("Looking for SIM changes. Credentials ICCS ${creds} " +
                  "Device ICCS ${device}", { creds: creds.deviceIccIds,
                                             device: this.iccIds });
        let simChanged = (creds.deviceIccIds == null && this.iccIds != null) ||
                         (creds.deviceIccIds != null && this.iccIds == null);

        if (!simChanged &&
            creds.deviceIccIds != null &&
            this.iccIds != null) {
          simChanged = creds.deviceIccIds.length != this.iccIds.length;
        }

        if (!simChanged &&
            creds.deviceIccIds != null &&
            this.iccIds != null) {
          let intersection = creds.deviceIccIds.filter((n) => {
            return this.iccIds.indexOf(n) != -1;
          });
          simChanged = intersection.length != creds.deviceIccIds.length ||
                       intersection.length != this.iccIds.length;
        }

        if (!simChanged) {
          return creds;
        }

        // At this point we know that the SIM associated with the credentials
        // is not present in the device any more or a new SIM has been detected,
        // so we need to ask the user what to do.
        return this.promptAndVerify(principal, manifestURL, creds)
        .then(
          (newCreds) => {
            return this.checkNewCredentials(creds, newCreds,
                                            principal.origin);
          }
        );
      }
    )
    .then(
      (creds) => {
        // Even if we have credentails it is possible that the user has
        // removed the permission to share its mobile id with this origin, so
        // we check the permission and if it is not granted, we ask the user
        // before generating and sharing the assertion.
        // If we've just prompted the user in the previous step, the permission
        // is already granted and stored so we just progress the credentials.
        if (creds) {
          if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
            return creds;
          }
          return this.promptAndVerify(principal, manifestURL, creds);
        }
        return this.promptAndVerify(principal, manifestURL);
      }
    )
    .then(
      (creds) => {
        if (creds) {
          return this.generateAssertion(creds, principal.origin);
        }
        return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
      }
    )
    .then(
      (assertion) => {
        if (!assertion) {
          return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
        }

        // Get the verified phone number from the assertion.
        let segments = assertion.split(".");
        if (!segments) {
          return Promise.reject(ERROR_INVALID_ASSERTION);
        }

        // We need to translate the base64 alphabet used in JWT to our base64
        // alphabet before calling atob.
        let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
                                                        .replace(/_/g, '/')));

        if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
          return Promise.reject(ERROR_INVALID_ASSERTION);
        }

        this.ui.verified(decodedPayload.verifiedMSISDN);

        this.success(aPromiseId, assertion);
      }
    )
    .then(
      null,
      (error) => {
        log.error("getMobileIdAssertion rejected with ${}", error);

        // If we got an invalid token error means that the credentials that
        // we have are not valid anymore and so we need to refresh them. We
        // do that removing the stored credentials and starting over. We also
        // make sure that we do this only once.
        if (error === ERROR_INVALID_AUTH_TOKEN &&
            !aOptions.refreshCredentials) {
          log.debug("Need to get new credentials");
          aOptions.refreshCredentials = true;
          _creds && this.credStore.delete(_creds.msisdn);
          this.getMobileIdAssertion(aPrincipal, aPromiseId, aOptions);
          return;
        }

        // Notify the error to the UI.
        this.ui.error(error);

        this.error(aPromiseId, error);
      }
    );
  },

};

MobileIdentityManager.init();