gecko/services/mobileid/MobileIdentityManager.jsm
Wes Kocher 2c9ce1520b Backed out 17 changesets (bug 843452)
Backed out changeset e72a0297d488 (bug 843452)
Backed out changeset 73f62ce4190f (bug 843452)
Backed out changeset 6d4a49ebd9fc (bug 843452)
Backed out changeset ac93e396669f (bug 843452)
Backed out changeset cf4fc721821e (bug 843452)
Backed out changeset 9a94ea71d232 (bug 843452)
Backed out changeset b95ff097374d (bug 843452)
Backed out changeset 8d6428a93500 (bug 843452)
Backed out changeset e31b86ef0e80 (bug 843452)
Backed out changeset bd4efde535cd (bug 843452)
Backed out changeset 02bbcd8ac571 (bug 843452)
Backed out changeset f7f41bf82b22 (bug 843452)
Backed out changeset 20f42764cd38 (bug 843452)
Backed out changeset 82f8670f5c17 (bug 843452)
Backed out changeset 52f25f1278e3 (bug 843452)
Backed out changeset 181337820a7c (bug 843452)
Backed out changeset 9bd12641af03 (bug 843452)

--HG--
rename : dom/mobileconnection/interfaces/nsIMobileConnectionService.idl => dom/mobileconnection/interfaces/nsIMobileConnectionProvider.idl
2014-08-28 16:51:41 -07:00

978 lines
32 KiB
JavaScript

/* 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, "gRil",
"@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
********************************************************/
get iccInfo() {
if (this._iccInfo) {
return this._iccInfo;
}
#ifdef MOZ_B2G_RIL
this._iccInfo = [];
for (let i = 0; i < gRil.numRadioInterfaces; i++) {
let rilContext = gRil.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 voice = mobileConnectionService.getVoiceConnectionInfo(i);
let data = mobileConnectionService.getDataConnectionInfo(i);
let operator = null;
if (voice.network &&
voice.network.shortName &&
voice.network.shortName.length) {
operator = voice.network.shortName;
} else if (data.network &&
data.network.shortName &&
data.network.shortName.length) {
operator = data.network.shortName;
}
this._iccInfo.push({
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,
serviceId: i,
roaming: voice.roaming
});
}
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 to 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 " + aPrincipal + ", " + aManifestURL + ", " +
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, we don't allow the user to
// choose this option.
if (!this.iccInfo[i].msisdn && !this.iccInfo[i].credentials &&
!this.iccInfo[i].canDoSilentVerification) {
continue;
}
let phoneInfo = new MobileIdentityUIGluePhoneInfo(
this.iccInfo[i].msisdn,
this.iccInfo[i].operator,
i, // service ID
false, // external
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, // external
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();