Bumping manifests a=b2g-bump
[gecko.git] / services / mobileid / MobileIdentityManager.jsm
blobcefb6a4a10a9136e81e038cf083c43b66ecc5d92
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["MobileIdentityManager"];
9 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
11 Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
12 Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
13 Cu.import("resource://gre/modules/Promise.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityCredentialsStore",
17   "resource://gre/modules/MobileIdentityCredentialsStore.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityClient",
20   "resource://gre/modules/MobileIdentityClient.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMtVerificationFlow",
23   "resource://gre/modules/MobileIdentitySmsMtVerificationFlow.jsm");
25 XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMoMtVerificationFlow",
26   "resource://gre/modules/MobileIdentitySmsMoMtVerificationFlow.jsm");
28 XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
29   "resource://gre/modules/PhoneNumberUtils.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
32   "resource://gre/modules/identity/jwcrypto.jsm");
34 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
35                                    "@mozilla.org/uuid-generator;1",
36                                    "nsIUUIDGenerator");
38 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
39                                    "@mozilla.org/parentprocessmessagemanager;1",
40                                    "nsIMessageListenerManager");
42 XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
43                                    "@mozilla.org/permissionmanager;1",
44                                    "nsIPermissionManager");
46 XPCOMUtils.defineLazyServiceGetter(this, "securityManager",
47                                    "@mozilla.org/scriptsecuritymanager;1",
48                                    "nsIScriptSecurityManager");
50 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
51                                    "@mozilla.org/AppsService;1",
52                                    "nsIAppsService");
54 #ifdef MOZ_B2G_RIL
55 XPCOMUtils.defineLazyServiceGetter(this, "Ril",
56                                    "@mozilla.org/ril;1",
57                                    "nsIRadioInterfaceLayer");
59 XPCOMUtils.defineLazyServiceGetter(this, "IccProvider",
60                                    "@mozilla.org/ril/content-helper;1",
61                                    "nsIIccProvider");
63 XPCOMUtils.defineLazyServiceGetter(this, "MobileConnectionService",
64                                    "@mozilla.org/mobileconnection/mobileconnectionservice;1",
65                                    "nsIMobileConnectionService");
66 #endif
69 this.MobileIdentityManager = {
71   init: function() {
72     log.debug("MobileIdentityManager init");
73     Services.obs.addObserver(this, "xpcom-shutdown", false);
74     ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
75     this.messageManagers = {};
76     this.keyPairs = {};
77     this.certificates = {};
78   },
80   receiveMessage: function(aMessage) {
81     log.debug("Received " + aMessage.name);
83     if (aMessage.name !== GET_ASSERTION_IPC_MSG) {
84       return;
85     }
87     let msg = aMessage.json;
89     // We save the message target message manager so we can later dispatch
90     // back messages without broadcasting to all child processes.
91     let promiseId = msg.promiseId;
92     this.messageManagers[promiseId] = aMessage.target;
94     this.getMobileIdAssertion(aMessage.principal, promiseId, msg.options);
95   },
97   observe: function(subject, topic, data) {
98     if (topic != "xpcom-shutdown") {
99       return;
100     }
102     ppmm.removeMessageListener(GET_ASSERTION_IPC_MSG, this);
103     Services.obs.removeObserver(this, "xpcom-shutdown");
104     this.messageManagers = null;
105   },
107   /*********************************************************
108    * Getters
109    ********************************************************/
110 #ifdef MOZ_B2G_RIL
111   // We have these getters to allow mocking RIL stuff from the tests.
112   get ril() {
113     if (this._ril) {
114       return this._ril;
115     }
116     return Ril;
117   },
119   get iccProvider() {
120     if (this._iccProvider) {
121       return this._iccProvider;
122     }
123     return IccProvider;
124   },
126   get mobileConnectionService() {
127     if (this._mobileConnectionService) {
128       return this._mobileConnectionService;
129     }
130     return MobileConnectionService;
131   },
132 #endif
134   get iccInfo() {
135     if (this._iccInfo) {
136       return this._iccInfo;
137     }
138 #ifdef MOZ_B2G_RIL
139     let self = this;
140     let iccListener = {
141       notifyStkCommand: function() {},
143       notifyStkSessionEnd: function() {},
145       notifyCardStateChanged: function() {},
147       notifyIccInfoChanged: function() {
148         // If we receive a notification about an ICC info change, we clear
149         // the ICC related caches so they can be rebuilt with the new changes.
151         log.debug("ICC info changed observed. Clearing caches");
153         // We don't need to keep listening for changes until we rebuild the
154         // cache again.
155         for (let i = 0; i < self._iccInfo.length; i++) {
156           self.iccProvider.unregisterIccMsg(self._iccInfo[i].clientId,
157                                             iccListener);
158         }
160         self._iccInfo = null;
161         self._iccIds = null;
162       }
163     };
165     // _iccInfo is a local cache containing the information about the SIM cards
166     // that is interesting for the Mobile ID flow.
167     // The index of this array does not necesarily need to match the real
168     // identifier of the SIM card ("clientId" or "serviceId" in RIL language).
169     this._iccInfo = [];
171     for (let i = 0; i < this.ril.numRadioInterfaces; i++) {
172       let rilContext = this.ril.getRadioInterface(i).rilContext;
173       if (!rilContext) {
174         log.warn("Tried to get the RIL context for an invalid service ID " + i);
175         continue;
176       }
178       let info = rilContext.iccInfo;
179       if (!info || !info.iccid ||
180           !info.mcc || !info.mcc.length ||
181           !info.mnc || !info.mnc.length) {
182         log.warn("Absent or invalid ICC info");
183         continue;
184       }
186       let connection = this.mobileConnectionService.getItemByServiceId(i);
187       let voice = connection && connection.voice;
188       let data = connection && connection.data;
189       let operator = null;
190       if (voice &&
191           voice.network &&
192           voice.network.shortName &&
193           voice.network.shortName.length) {
194         operator = voice.network.shortName;
195       } else if (data &&
196                  data.network &&
197                  data.network.shortName &&
198                  data.network.shortName.length) {
199         operator = data.network.shortName;
200       }
202       this._iccInfo.push({
203         // Because it is possible that the _iccInfo array index doesn't match
204         // the real client ID, we need to store this value for later usage.
205         clientId: i,
206         iccId: info.iccid,
207         mcc: info.mcc,
208         mnc: info.mnc,
209         // GSM SIMs may have MSISDN while CDMA SIMs may have MDN
210         msisdn: info.msisdn || info.mdn || null,
211         operator: operator,
212         roaming: voice && voice.roaming
213       });
215       // We need to subscribe to ICC change notifications so we can refresh
216       // the cache if any change is observed.
217       this.iccProvider.registerIccMsg(i, iccListener);
218     }
220     return this._iccInfo;
221 #endif
222     return null;
223   },
225   get iccIds() {
226 #ifdef MOZ_B2G_RIL
227     if (this._iccIds) {
228       return this._iccIds;
229     }
231     this._iccIds = [];
232     if (!this.iccInfo) {
233       return this._iccIds;
234     }
236     for (let i = 0; i < this.iccInfo.length; i++) {
237       this._iccIds.push(this.iccInfo[i].iccId);
238     }
240     return this._iccIds;
241 #endif
242     return null;
243   },
245   get credStore() {
246     if (!this._credStore) {
247       this._credStore = new MobileIdentityCredentialsStore();
248       this._credStore.init();
249     }
250     return this._credStore;
251   },
253   get ui() {
254     if (!this._ui) {
255       this._ui = Cc["@mozilla.org/services/mobileid-ui-glue;1"]
256                    .createInstance(Ci.nsIMobileIdentityUIGlue);
257       this._ui.oncancel = this.onUICancel.bind(this);
258       this._ui.onresendcode = this.onUIResendCode.bind(this);
259     }
260     return this._ui;
261   },
263   get client() {
264     if (!this._client) {
265       this._client = new MobileIdentityClient();
266     }
267     return this._client;
268   },
270   get isMultiSim() {
271     return this.iccInfo && this.iccInfo.length > 1;
272   },
274   getVerificationOptionsForIcc: function(aServiceId) {
275     log.debug("getVerificationOptionsForIcc " + aServiceId);
276     log.debug("iccInfo ${}", this.iccInfo[aServiceId]);
277     // First of all we need to check if we already have existing credentials
278     // for the given SIM information (ICC ID or MSISDN). If we have no valid
279     // credentials, we have to check with the server which options do we have
280     // to verify the associated phone number.
281     return this.credStore.getByIccId(this.iccInfo[aServiceId].iccId)
282     .then(
283       (creds) => {
284         if (creds) {
285           return creds;
286         }
287         return this.credStore.getByMsisdn(this.iccInfo[aServiceId].msisdn);
288       }
289     )
290     .then(
291       (creds) => {
292         if (creds) {
293           this.iccInfo[aServiceId].credentials = creds;
294           return;
295         }
296         // We have no credentials for this SIM, so we need to ask the server
297         // which options do we have to verify the phone number.
298         // But we need to be online...
299         if (Services.io.offline) {
300           return Promise.reject(ERROR_OFFLINE);
301         }
302         return this.client.discover(this.iccInfo[aServiceId].msisdn,
303                                     this.iccInfo[aServiceId].mcc,
304                                     this.iccInfo[aServiceId].mnc,
305                                     this.iccInfo[aServiceId].roaming);
306       }
307     )
308     .then(
309       (result) => {
310         // If we already have credentials for this ICC and no discover request
311         // is done, we just bail out.
312         if (!result || !result.verificationMethods) {
313           return;
314         }
315         log.debug("Discover result ${}", result);
316         this.iccInfo[aServiceId].verificationMethods = result.verificationMethods;
317         this.iccInfo[aServiceId].verificationDetails = result.verificationDetails;
318         this.iccInfo[aServiceId].canDoSilentVerification =
319           (result.verificationMethods.indexOf(SMS_MO_MT) != -1);
320         return;
321       }
322     );
323   },
325   getVerificationOptions: function() {
326     log.debug("getVerificationOptions");
327     // We try to get if we already have credentials for any of the inserted
328     // SIM cards if any is available and we try to get the possible
329     // verification mechanisms for these SIM cards.
330     // All this information will be stored in iccInfo.
331     if (!this.iccInfo || !this.iccInfo.length) {
332       let deferred = Promise.defer();
333       deferred.resolve(null);
334       return deferred.promise;
335     }
337     let promises = [];
338     for (let i = 0; i < this.iccInfo.length; i++) {
339       promises.push(this.getVerificationOptionsForIcc(i));
340     }
341     return Promise.all(promises);
342   },
344   getKeyPair: function(aSessionToken) {
345     if (this.keyPairs[aSessionToken] &&
346         this.keyPairs[aSessionToken].validUntil > this.client.hawk.now()) {
347       return Promise.resolve(this.keyPairs[aSessionToken].keyPair);
348     }
350     let validUntil = this.client.hawk.now() + KEY_LIFETIME;
351     let deferred = Promise.defer();
352     jwcrypto.generateKeyPair("DS160", (error, kp) => {
353       if (error) {
354         return deferred.reject(error);
355       }
356       this.keyPairs[aSessionToken] = {
357         keyPair: kp,
358         validUntil: validUntil
359       };
360       delete this.certificates[aSessionToken];
361       deferred.resolve(kp);
362     });
364     return deferred.promise;
365   },
367   getCertificate: function(aSessionToken, aPublicKey) {
368     log.debug("getCertificate");
369     if (this.certificates[aSessionToken] &&
370         this.certificates[aSessionToken].validUntil > this.client.hawk.now()) {
371       return Promise.resolve(this.certificates[aSessionToken].cert);
372     }
374     if (Services.io.offline) {
375       return Promise.reject(ERROR_OFFLINE);
376     }
378     let validUntil = this.client.hawk.now() + KEY_LIFETIME;
379     let deferred = Promise.defer();
380     this.client.sign(aSessionToken, CERTIFICATE_LIFETIME,
381                      aPublicKey)
382     .then(
383       (signedCert) => {
384         log.debug("Got signed certificate");
385         this.certificates[aSessionToken] = {
386           cert: signedCert.cert,
387           validUntil: validUntil
388         };
389         deferred.resolve(signedCert.cert);
390       },
391       deferred.reject
392     );
393     return deferred.promise;
394   },
396   /*********************************************************
397    * Setters (for test only purposes)
398    ********************************************************/
399   set ui(aUi) {
400     this._ui = aUi;
401   },
403   set credStore(aCredStore) {
404     this._credStore = aCredStore;
405   },
407   set client(aClient) {
408     this._client = aClient;
409   },
411   set iccInfo(aIccInfo) {
412     this._iccInfo = aIccInfo;
413   },
415   /*********************************************************
416    * UI callbacks
417    ********************************************************/
419   onUICancel: function() {
420     log.debug("UI cancel");
421     if (this.activeVerificationFlow) {
422       this.rejectVerification();
423     }
424   },
426   onUIResendCode: function() {
427     log.debug("UI resend code");
428     if (!this.activeVerificationFlow) {
429       return;
430     }
431     this.doVerification();
432   },
434   /*********************************************************
435    * Result helpers
436    *********************************************************/
437   success: function(aPromiseId, aResult) {
438     let mm = this.messageManagers[aPromiseId];
439     mm.sendAsyncMessage("MobileId:GetAssertion:Return:OK", {
440       promiseId: aPromiseId,
441       result: aResult
442     });
443   },
445   error: function(aPromiseId, aError) {
446     let mm = this.messageManagers[aPromiseId];
447     mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
448       promiseId: aPromiseId,
449       error: aError
450     });
451   },
453   /*********************************************************
454    * Permissions helper
455    ********************************************************/
457   addPermission: function(aPrincipal) {
458     permissionManager.addFromPrincipal(aPrincipal, MOBILEID_PERM,
459                                        Ci.nsIPermissionManager.ALLOW_ACTION);
460   },
462   /*********************************************************
463    * Phone number verification
464    ********************************************************/
466   rejectVerification: function(aReason) {
467     if (!this.activeVerificationDeferred) {
468       return;
469     }
470     this.activeVerificationDeferred.reject(aReason);
471     this.activeVerificationDeferred = null;
472     this.cleanupVerification(true /* unregister */);
473   },
475   resolveVerification: function(aResult) {
476     if (!this.activeVerificationDeferred) {
477       return;
478     }
479     this.activeVerificationDeferred.resolve(aResult);
480     this.activeVerificationDeferred = null;
481     this.cleanupVerification();
482   },
484   cleanupVerification: function(aUnregister = false) {
485     if (!this.activeVerificationFlow) {
486       return;
487     }
488     this.activeVerificationFlow.cleanup(aUnregister);
489     this.activeVerificationFlow = null;
490   },
492   doVerification: function() {
493     this.activeVerificationFlow.doVerification()
494     .then(
495       (verificationResult) => {
496         log.debug("onVerificationResult ");
497         if (!verificationResult || !verificationResult.sessionToken ||
498             !verificationResult.msisdn) {
499           return this.rejectVerification(
500             ERROR_INTERNAL_INVALID_VERIFICATION_RESULT
501           );
502         }
503         this.resolveVerification(verificationResult);
504       }
505     )
506     .then(
507       null,
508       reason => {
509         // Verification timeout.
510         log.warn("doVerification " + reason);
511       }
512     );
513   },
515   _verificationFlow: function(aToVerify, aOrigin) {
516     log.debug("toVerify ${}", aToVerify);
518     // We create the corresponding verification flow and save its instance
519     // in case that we need to cancel it or retrigger it because the user
520     // requested its cancelation or a resend of the verification code.
521     if (aToVerify.verificationMethod.indexOf(SMS_MT) != -1 &&
522         aToVerify.msisdn &&
523         aToVerify.verificationDetails &&
524         aToVerify.verificationDetails.mtSender) {
525       this.activeVerificationFlow = new MobileIdentitySmsMtVerificationFlow({
526           origin: aOrigin,
527           msisdn: aToVerify.msisdn,
528           mcc: aToVerify.mcc,
529           mnc: aToVerify.mnc,
530           iccId: aToVerify.iccId,
531           external: aToVerify.serviceId === undefined,
532           mtSender: aToVerify.verificationDetails.mtSender
533         },
534         this.ui,
535         this.client
536       );
537 #ifdef MOZ_B2G_RIL
538     } else if (aToVerify.verificationMethod.indexOf(SMS_MO_MT) != -1 &&
539                aToVerify.serviceId &&
540                aToVerify.verificationDetails &&
541                aToVerify.verificationDetails.moVerifier &&
542                aToVerify.verificationDetails.mtSender) {
543       this.activeVerificationFlow = new MobileIdentitySmsMoMtVerificationFlow({
544           origin: aOrigin,
545           serviceId: aToVerify.serviceId,
546           iccId: aToVerify.iccId,
547           mtSender: aToVerify.verificationDetails.mtSender,
548           moVerifier: aToVerify.verificationDetails.moVerifier
549         },
550         this.ui,
551         this.client
552       );
553 #endif
554     } else {
555       return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
556     }
558     if (!this.activeVerificationFlow) {
559       return Promise.reject(ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW);
560     }
562     this.activeVerificationDeferred = Promise.defer();
563     this.doVerification();
564     return this.activeVerificationDeferred.promise;
565   },
567   verificationFlow: function(aUserSelection, aOrigin) {
568     log.debug("verificationFlow ${}", aUserSelection);
570     if (!aUserSelection) {
571       return Promise.reject(ERROR_INTERNAL_INVALID_USER_SELECTION);
572     }
574     let serviceId = aUserSelection.serviceId || undefined;
575     // We check if the user entered phone number corresponds with any of the
576     // inserted SIMs known phone numbers.
577     if (aUserSelection.msisdn && this.iccInfo) {
578       for (let i = 0; i < this.iccInfo.length; i++) {
579         if (aUserSelection.msisdn == this.iccInfo[i].msisdn) {
580           serviceId = i;
581           break;
582         }
583       }
584     }
586     let toVerify = {};
588     if (serviceId !== undefined) {
589       log.debug("iccInfo ${}", this.iccInfo[serviceId]);
590       toVerify.serviceId = serviceId;
591       toVerify.iccId = this.iccInfo[serviceId].iccId;
592       toVerify.msisdn = this.iccInfo[serviceId].msisdn;
593       toVerify.mcc = this.iccInfo[serviceId].mcc;
594       toVerify.mnc = this.iccInfo[serviceId].mnc;
595       toVerify.verificationMethod =
596         this.iccInfo[serviceId].verificationMethods[0];
597       toVerify.verificationDetails =
598         this.iccInfo[serviceId].verificationDetails[toVerify.verificationMethod];
599       return this._verificationFlow(toVerify, aOrigin);
600     } else {
601       toVerify.msisdn = aUserSelection.msisdn;
602       toVerify.mcc = aUserSelection.mcc;
603       return this.client.discover(aUserSelection.msisdn,
604                                   aUserSelection.mcc)
605       .then(
606         (discoverResult) => {
607           if (!discoverResult || !discoverResult.verificationMethods) {
608             return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
609           }
610           log.debug("discoverResult ${}", discoverResult);
611           toVerify.verificationMethod = discoverResult.verificationMethods[0];
612           toVerify.verificationDetails =
613             discoverResult.verificationDetails[toVerify.verificationMethod];
614           return this._verificationFlow(toVerify, aOrigin);
615         }
616       );
617     }
618   },
621   /*********************************************************
622    * UI prompt functions.
623    ********************************************************/
625   // The phone number prompt will be used to confirm that the user wants to
626   // verify and share a known phone number and to allow her to introduce an
627   // external phone or to select between phone numbers or SIM cards (if the
628   // phones are not known) in a multi-SIM scenario.
629   // This prompt will be considered as the permission prompt and its choice
630   // will be remembered per origin by default.
631   prompt: function prompt(aPrincipal, aManifestURL, aPhoneInfo) {
632     log.debug("prompt ${principal} ${manifest} ${phoneInfo}", {
633       principal: aPrincipal,
634       manifest: aManifestURL,
635       phoneInfo: aPhoneInfo
636     });
638     let phoneInfoArray = [];
640     if (aPhoneInfo) {
641       phoneInfoArray.push(aPhoneInfo);
642     }
644     if (this.iccInfo) {
645       for (let i = 0; i < this.iccInfo.length; i++) {
646         // If we don't know the msisdn, there is no previous credentials and
647         // a silent verification is not possible or if the msisdn is the one
648         // that is already chosen, we don't list this SIM as an option.
649         if ((!this.iccInfo[i].msisdn && !this.iccInfo[i].credentials &&
650             !this.iccInfo[i].canDoSilentVerification) ||
651             ((aPhoneInfo) &&
652              (this.iccInfo[i].msisdn == aPhoneInfo.msisdn ||
653               this.iccInfo[i].iccId == aPhoneInfo.iccId))) {
654           continue;
655         }
657         let phoneInfo = new MobileIdentityUIGluePhoneInfo(
658           this.iccInfo[i].msisdn,
659           this.iccInfo[i].operator,
660           i,                      // service ID
661           this.iccInfo[i].iccId,  // iccId
662           false                   // primary
663         );
664         phoneInfoArray.push(phoneInfo);
665       }
666     }
668     return this.ui.startFlow(aManifestURL, phoneInfoArray)
669     .then(
670       (result) => {
671         log.debug("startFlow result ${} ", result);
672         if (!result ||
673             (!result.phoneNumber && (result.serviceId === undefined))) {
674           return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
675         }
677         let msisdn;
678         let mcc;
680         // If the user selected one of the existing SIM cards we have to check
681         // that we either have the MSISDN for that SIM, we have already existing
682         // credentials or we can do a silent verification that does not require
683         // us to have the MSISDN in advance.
684         // result.serviceId can be "0".
685         if (result.serviceId !== undefined &&
686             result.serviceId !== null) {
687           let icc = this.iccInfo[result.serviceId];
688           log.debug("icc ${}", icc);
689           if (!icc || !icc.msisdn && !icc.canDoSilentVerification &&
690               !icc.credentials) {
691             return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
692           }
693           msisdn = icc.msisdn;
694           mcc = icc.mcc;
695         } else {
696           msisdn = result.prefix ? result.prefix + result.phoneNumber
697                                  : result.phoneNumber;
698           mcc = result.mcc;
699         }
701         // We need to check that the selected phone number is valid and
702         // if it is not notify the UI about the error and allow the user to
703         // retry.
704         if (msisdn && mcc &&
705             !PhoneNumberUtils.parseWithMCC(msisdn, mcc)) {
706           this.ui.error(ERROR_INVALID_PHONE_NUMBER);
707           return this.prompt(aPrincipal, aManifestURL, aPhoneInfo);
708         }
710         log.debug("Selected msisdn (if any): " + msisdn + " - " + mcc);
712         // The user gave permission for the requester origin, so we store it.
713         this.addPermission(aPrincipal);
715         return {
716           msisdn: msisdn,
717           mcc: mcc,
718           serviceId: result.serviceId
719         };
720       }
721     );
722   },
724   promptAndVerify: function(aPrincipal, aManifestURL, aCreds) {
725     log.debug("promptAndVerify " + aPrincipal + ", " + aManifestURL +
726               ", ${}", aCreds);
727     let userSelection;
729     if (Services.io.offline) {
730       return Promise.reject(ERROR_OFFLINE);
731     }
733     // Before prompting the user we need to check with the server the
734     // phone number verification methods that are possible with the
735     // SIMs inserted in the device.
736     return this.getVerificationOptions()
737     .then(
738       () => {
739         // If we have an exisiting credentials, we add its associated
740         // phone number information to the list of choices to present
741         // to the user within the selection prompt.
742         let phoneInfo;
743         if (aCreds) {
744           phoneInfo = new MobileIdentityUIGluePhoneInfo(
745             aCreds.msisdn,
746             null,           // operator
747             undefined,      // service ID
748             aCreds.iccId,   // iccId
749             true            // primary
750           );
751         }
752         return this.prompt(aPrincipal, aManifestURL, phoneInfo);
753       }
754     )
755     .then(
756       (promptResult) => {
757         log.debug("promptResult ${}", promptResult);
758         // If we had credentials and the user didn't change her
759         // selection we return them. Otherwise, we need to verify
760         // the new number.
761         if (promptResult.msisdn && aCreds &&
762             promptResult.msisdn == aCreds.msisdn) {
763           return aCreds;
764         }
766         // We might already have credentials for the user selected icc. In
767         // that case, we update the credentials store with the new origin and
768         // return the credentials.
769         if (promptResult.serviceId) {
770           let creds = this.iccInfo[promptResult.serviceId].credentials;
771           if (creds) {
772             this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
773                                creds.sessionToken, this.iccIds);
774             return creds;
775           }
776         }
778         // Or we might already have credentials for the selected phone
779         // number and so we do the same: update the credentials store with the
780         // new origin and return the credentials.
781         return this.credStore.getByMsisdn(promptResult.msisdn)
782         .then(
783           (creds) => {
784             if (creds) {
785               this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
786                                  creds.sessionToken, this.iccIds);
787               return creds;
788             }
789             // Otherwise, we need to verify the new number selected by the
790             // user.
791             return this.verificationFlow(promptResult, aPrincipal.origin);
792           }
793         );
794       }
795     );
796   },
798   /*********************************************************
799    * Credentials check
800    *********************************************************/
802   checkNewCredentials: function(aOldCreds, aNewCreds, aOrigin) {
803     // If there were previous credentials and the user changed her
804     // choice, we need to remove the origin from the old credentials.
805     if (aNewCreds.msisdn != aOldCreds.msisdn) {
806       return this.credStore.removeOrigin(aOldCreds.msisdn,
807                                          aOrigin)
808       .then(
809         () => {
810           return aNewCreds;
811         }
812       );
813     } else {
814       // Otherwise, we update the status of the SIM cards in the device
815       // so we know that the user decided not to take the chance to change
816       // her selection. We won't bother her again until a new SIM card
817       // change is detected.
818       return this.credStore.setDeviceIccIds(aOldCreds.msisdn, this.iccIds)
819       .then(
820         () => {
821           return aOldCreds;
822         }
823       );
824     }
825   },
827   /*********************************************************
828    * Assertion generation
829    ********************************************************/
831   generateAssertion: function(aCredentials, aOrigin) {
832     if (!aCredentials.sessionToken) {
833       return Promise.reject(ERROR_INTERNAL_INVALID_TOKEN);
834     }
836     let deferred = Promise.defer();
838     this.getKeyPair(aCredentials.sessionToken)
839     .then(
840       (keyPair) => {
841         log.debug("keyPair " + keyPair.serializedPublicKey);
842         let options = {
843           duration: ASSERTION_LIFETIME,
844           now: this.client.hawk.now(),
845           localtimeOffsetMsec: this.client.hawk.localtimeOffsetMsec
846         };
848         this.getCertificate(aCredentials.sessionToken,
849                             keyPair.serializedPublicKey)
850         .then(
851           (signedCert) => {
852             log.debug("generateAssertion " + signedCert);
853             jwcrypto.generateAssertion(signedCert, keyPair,
854                                        aOrigin, options,
855                                        (error, assertion) => {
856               if (error) {
857                 log.error("Error generating assertion " + err);
858                 deferred.reject(error);
859                 return;
860               }
861               this.credStore.add(aCredentials.iccId,
862                                  aCredentials.msisdn,
863                                  aOrigin,
864                                  aCredentials.sessionToken,
865                                  this.iccIds)
866               .then(
867                 () => {
868                   deferred.resolve(assertion);
869                 }
870               );
871             });
872           }, deferred.reject
873         );
874       }
875     );
877     return deferred.promise;
878   },
880   getMobileIdAssertion: function(aPrincipal, aPromiseId, aOptions) {
881     log.debug("getMobileIdAssertion ${}", aPrincipal);
883     let uri = Services.io.newURI(aPrincipal.origin, null, null);
884     let principal = securityManager.getAppCodebasePrincipal(
885       uri, aPrincipal.appId, aPrincipal.isInBrowserElement);
886     let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);
888     let permission = permissionManager.testPermissionFromPrincipal(
889       principal,
890       MOBILEID_PERM
891     );
893     if (permission == Ci.nsIPermissionManager.DENY_ACTION ||
894         permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
895       this.error(aPromiseId, ERROR_PERMISSION_DENIED);
896       return;
897     }
899     let _creds;
901     // First of all we look if we already have credentials for this origin.
902     // If we don't have credentials it means that it is the first time that
903     // the caller requested an assertion.
904     this.credStore.getByOrigin(aPrincipal.origin)
905     .then(
906       (creds) => {
907         log.debug("creds ${creds} - ${origin}", { creds: creds,
908                                                   origin: aPrincipal.origin });
909         if (!creds || !creds.sessionToken) {
910           log.debug("No credentials");
911           return;
912         }
914         _creds = creds;
916         // Even if we already have credentials for this origin, the consumer
917         // of the API might want to force the identity selection dialog.
918         if (aOptions.forceSelection || aOptions.refreshCredentials) {
919           return this.promptAndVerify(principal, manifestURL, creds)
920           .then(
921             (newCreds) => {
922               return this.checkNewCredentials(creds, newCreds,
923                                               principal.origin);
924             }
925           );
926         }
928         // SIM change scenario.
930         // It is possible that the SIM cards inserted in the device at the
931         // moment of the previous verification where we obtained the credentials
932         // has changed. In that case, we need to let the user knowabout this
933         // situation. Otherwise, we just return the credentials.
934         log.debug("Looking for SIM changes. Credentials ICCS ${creds} " +
935                   "Device ICCS ${device}", { creds: creds.deviceIccIds,
936                                              device: this.iccIds });
937         let simChanged = (creds.deviceIccIds == null && this.iccIds != null) ||
938                          (creds.deviceIccIds != null && this.iccIds == null);
940         if (!simChanged &&
941             creds.deviceIccIds != null &&
942             this.iccIds != null) {
943           simChanged = creds.deviceIccIds.length != this.iccIds.length;
944         }
946         if (!simChanged &&
947             creds.deviceIccIds != null &&
948             this.iccIds != null) {
949           let intersection = creds.deviceIccIds.filter((n) => {
950             return this.iccIds.indexOf(n) != -1;
951           });
952           simChanged = intersection.length != creds.deviceIccIds.length ||
953                        intersection.length != this.iccIds.length;
954         }
956         if (!simChanged) {
957           return creds;
958         }
960         // At this point we know that the SIM associated with the credentials
961         // is not present in the device any more or a new SIM has been detected,
962         // so we need to ask the user what to do.
963         return this.promptAndVerify(principal, manifestURL, creds)
964         .then(
965           (newCreds) => {
966             return this.checkNewCredentials(creds, newCreds,
967                                             principal.origin);
968           }
969         );
970       }
971     )
972     .then(
973       (creds) => {
974         // Even if we have credentails it is possible that the user has
975         // removed the permission to share its mobile id with this origin, so
976         // we check the permission and if it is not granted, we ask the user
977         // before generating and sharing the assertion.
978         // If we've just prompted the user in the previous step, the permission
979         // is already granted and stored so we just progress the credentials.
980         // But we have to refresh the cached permission before checking.
981         if (creds) {
982           permission = permissionManager.testPermissionFromPrincipal(
983             principal,
984             MOBILEID_PERM
985           );
986           if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
987             return creds;
988           }
989           return this.promptAndVerify(principal, manifestURL, creds);
990         }
991         return this.promptAndVerify(principal, manifestURL);
992       }
993     )
994     .then(
995       (creds) => {
996         if (creds) {
997           return this.generateAssertion(creds, principal.origin);
998         }
999         return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
1000       }
1001     )
1002     .then(
1003       (assertion) => {
1004         if (!assertion) {
1005           return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
1006         }
1008         // Get the verified phone number from the assertion.
1009         let segments = assertion.split(".");
1010         if (!segments) {
1011           return Promise.reject(ERROR_INVALID_ASSERTION);
1012         }
1014         // We need to translate the base64 alphabet used in JWT to our base64
1015         // alphabet before calling atob.
1016         let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
1017                                                         .replace(/_/g, '/')));
1019         if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
1020           return Promise.reject(ERROR_INVALID_ASSERTION);
1021         }
1023         this.ui.verified(decodedPayload.verifiedMSISDN);
1025         this.success(aPromiseId, assertion);
1026       }
1027     )
1028     .then(
1029       null,
1030       (error) => {
1031         log.error("getMobileIdAssertion rejected with ${}", error);
1033         // If we got an invalid token error means that the credentials that
1034         // we have are not valid anymore and so we need to refresh them. We
1035         // do that removing the stored credentials and starting over. We also
1036         // make sure that we do this only once.
1037         if (error === ERROR_INVALID_AUTH_TOKEN &&
1038             !aOptions.refreshCredentials) {
1039           log.debug("Need to get new credentials");
1040           aOptions.refreshCredentials = true;
1041           _creds && this.credStore.delete(_creds.msisdn);
1042           this.getMobileIdAssertion(aPrincipal, aPromiseId, aOptions);
1043           return;
1044         }
1046         // Notify the error to the UI.
1047         this.ui.error(error);
1049         this.error(aPromiseId, error);
1050       }
1051     );
1052   },
1056 MobileIdentityManager.init();