Bug 988469 - MSISDN verification API for privileged apps. Part 4: Mobile ID service. r=markh, jedp

This commit is contained in:
Fernando Jiménez 2014-06-07 19:30:19 +02:00
parent 30b5c241ba
commit 24daeb1035
16 changed files with 1919 additions and 2 deletions

View File

@ -988,5 +988,7 @@ pref("services.sync.fxaccounts.enabled", true);
pref("identity.fxaccounts.enabled", true);
#endif
pref("services.mobileid.server.uri", "http://msisdn.dev.mozaws.net");
// Enable mapped array buffer
pref("dom.mapped_arraybuffer.enabled", true);

View File

@ -30,6 +30,7 @@ Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
#endif
Cu.import('resource://gre/modules/DownloadsAPI.jsm');
Cu.import('resource://gre/modules/MobileIdentityManager.jsm');
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
"resource://gre/modules/SystemAppProxy.jsm");

View File

@ -831,8 +831,9 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@
@BINPATH@/components/MobileIdentity.manifest
@BINPATH@/components/MobileIdentity.js
@BINPATH@/components/MobileIdentityUIGlue.js
@BINPATH@/components/dom_mobileidentity.xpt
@BINPATH@/components/MobileIdentityUIGlue.js
@BINPATH@/components/services_mobileidentity.xpt
#ifdef MOZ_WEBSPEECH
@BINPATH@/components/dom_webspeechsynth.xpt

View File

@ -0,0 +1,158 @@
/* 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/. */
// REST client for
// https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md
"use strict";
this.EXPORTED_SYMBOLS = ["MobileIdentityClient"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-common/hawkclient.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
this.MobileIdentityClient = function(aServerUrl) {
let serverUrl = aServerUrl || SERVER_URL;
let forceHttps = false;
try {
// TODO: Force https in production. Bug 1021595.
forceHttps = Services.prefs.getBoolPref(PREF_FORCE_HTTPS);
} catch(e) {
log.warn("Getting force HTTPS pref failed. If this was not intentional " +
"check that " + PREF_FORCE_HTTPS + " is defined");
}
log.debug("Force HTTPS " + forceHttps);
if (forceHttps && !/^https/.exec(serverUrl.toLowerCase())) {
throw new Error(ERROR_INTERNAL_HTTP_NOT_ALLOWED);
}
this.hawk = new HawkClient(SERVER_URL);
this.hawk.observerPrefix = "MobileId:hawk";
};
this.MobileIdentityClient.prototype = {
discover: function(aMsisdn, aMcc, aMnc, aRoaming) {
return this._request(DISCOVER, "POST", null, {
msisdn: aMsisdn || undefined,
mcc: aMcc,
mnc: aMnc,
roaming: aRoaming
});
},
register: function() {
return this._request(REGISTER, "POST", null, {});
},
smsMtVerify: function(aSessionToken, aMsisdn, aWantShortCode = false) {
let credentials = this._deriveHawkCredentials(aSessionToken);
return this._request(SMS_MT_VERIFY, "POST", credentials, {
msisdn: aMsisdn,
shortVerificationCode: aWantShortCode
});
},
verifyCode: function(aSessionToken, aVerificationCode) {
log.debug("verificationCode " + aVerificationCode);
let credentials = this._deriveHawkCredentials(aSessionToken);
return this._request(SMS_VERIFY_CODE, "POST", credentials, {
code: aVerificationCode
});
},
sign: function(aSessionToken, aDuration, aPublicKey) {
let credentials = this._deriveHawkCredentials(aSessionToken);
return this._request(SIGN, "POST", credentials, {
duration: aDuration,
publicKey: aPublicKey
});
},
unregister: function(aSessionToken) {
let credentials = this._deriveHawkCredentials(aSessionToken);
return this._request(UNREGISTER, "POST", credentials, {});
},
/**
* The MobileID server expects requests to certain endpoints to be
* authorized using Hawk.
*
* Hawk credentials are derived using shared secrets.
*
* @param tokenHex
* The current session token encoded in hex
* @param context
* A context for the credentials
* @param size
* The size in bytes of the expected derived buffer
* @return credentials
* Returns an object:
* {
* algorithm: sha256
* id: the Hawk id (from the first 32 bytes derived)
* key: the Hawk key (from bytes 32 to 64)
* }
*/
_deriveHawkCredentials: function(aSessionToken) {
let token = CommonUtils.hexToBytes(aSessionToken);
let out = CryptoUtils.hkdf(token, undefined,
CREDENTIALS_DERIVATION_INFO,
CREDENTIALS_DERIVATION_SIZE);
return {
algorithm: "sha256",
key: CommonUtils.bytesAsHex(out.slice(32, 64)),
id: CommonUtils.bytesAsHex(out.slice(0, 32))
};
},
/**
* A general method for sending raw API calls to the mobile id verification
* server.
* All request bodies and responses are JSON.
*
* @param path
* API endpoint path
* @param method
* The HTTP request method
* @param credentials
* Hawk credentials
* @param jsonPayload
* A JSON payload
* @return Promise
* Returns a promise that resolves to the JSON response of the API
* call, or is rejected with an error.
*/
_request: function(path, method, credentials, jsonPayload) {
let deferred = Promise.defer();
this.hawk.request(path, method, credentials, jsonPayload).then(
(responseText) => {
log.debug("MobileIdentityClient -> responseText " + responseText);
try {
let response = JSON.parse(responseText);
deferred.resolve(response);
} catch (err) {
deferred.reject({error: err});
}
},
(error) => {
log.error("MobileIdentityClient -> Error ${}", error);
deferred.reject(SERVER_ERRNO_TO_ERROR[error.errno] || ERROR_UNKNOWN);
}
);
return deferred.promise;
},
};

View File

@ -0,0 +1,143 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Log.jsm");
// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
// "Debug", "Trace" or "All". If none is specified, "Error" will be used by
// default.
const PREF_LOG_LEVEL = "services.mobileid.loglevel";
XPCOMUtils.defineLazyGetter(this, "log", function() {
let log = Log.repository.getLogger("MobileId");
log.addAppender(new Log.DumpAppender());
log.level = Log.Level.Error;
try {
let level =
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
log.level = Log.Level[level] || Log.Level.Error;
} catch (e) {
log.error(e);
}
return log;
});
this.PREF_FORCE_HTTPS = "services.mobileid.forcehttps";
// Permission.
this.MOBILEID_PERM = "mobileid";
// IPC messages.
this.GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
// Verification methods.
this.SMS_MT = "sms/mt";
this.SMS_MO_MT = "sms/momt";
// Server endpoints.
this.DISCOVER = "/discover";
this.REGISTER = "/register";
this.SMS_MT_VERIFY = "/" + this.SMS_MT + "/verify";
this.SMS_MO_MT_VERIFY = "/" + this.SMS_MO_MT + "/verify";
this.SMS_VERIFY_CODE = "/sms/verify_code";
this.SIGN = "/certificate/sign";
this.UNREGISTER = "/unregister";
// Server consts.
this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri");
this.CREDENTIALS_DERIVATION_INFO = "identity.mozilla.com/picl/v1/sessionToken";
this.CREDENTIALS_DERIVATION_SIZE = 2 * 32;
this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received";
this.ASSERTION_LIFETIME = 1000 * 60 * 5; // 5 minutes.
this.CERTIFICATE_LIFETIME = 1000 * 3600 * 6; // 6 hours.
this.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours.
this.VERIFICATIONCODE_TIMEOUT = 60000;
this.VERIFICATIONCODE_RETRIES = 3;
// Internal Errors.
this.ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW = "INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW";
this.ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION = "INTERNAL_CANNOT_GENERATE_ASSERTION";
this.ERROR_INTERNAL_CANNOT_VERIFY_SELECTION = "INTERNAL_CANNOT_VERIFY_SELECTION";
this.ERROR_INTERNAL_DB_ERROR = "INTERNAL_DB_ERROR";
this.ERROR_INTERNAL_HTTP_NOT_ALLOWED = "INTERNAL_HTTP_NOT_ALLOWED";
this.ERROR_INTERNAL_INVALID_CERTIFICATE = "INTERNAL_INVALID_CERTIFICATE";
this.ERROR_INTERNAL_INVALID_PROMPT_RESULT = "INTERNAL_INVALID_PROMPT_RESULT";
this.ERROR_INTERNAL_INVALID_USER_SELECTION = "INTERNAL_INVALID_USER_SELECTION";
this.ERROR_INTERNAL_INVALID_VERIFICATION_FLOW = "INTERNAL_INVALID_VERIFICATION_FLOW";
this.ERROR_INTERNAL_INVALID_VERIFICATION_RESULT = "INTERNAL_INVALID_VERIFICATION_RESULT";
this.ERROR_INTERNAL_UNEXPECTED = "INTERNAL_UNEXPECTED";
// Errors.
this.ERROR_ENDPOINT_NOT_SUPPORTED = "ENDPOINT_NOT_SUPPORTED";
this.ERROR_INVALID_ASSERTION = "INVALID_ASSERTION";
this.ERROR_INVALID_AUTH_TOKEN = "INVALID_AUTH_TOKEN";
this.ERROR_INVALID_BODY_JSON = "INVALID_BODY_JSON";
this.ERROR_INVALID_BODY_MISSING_PARAMS = "INVALID_BODY_MISSING_PARAMS";
this.ERROR_INVALID_BODY_PARAMS = "INVALID_BODY_PARAMS";
this.ERROR_INVALID_PHONE_NUMBER = "INVALID_PHONE_NUMBER";
this.ERROR_INVALID_PROMPT_RESULT = "INVALID_PROMPT_RESULT";
this.ERROR_INVALID_REQUEST_SIGNATURE = "INVALID_REQUEST_SIGNATURE";
this.ERROR_INVALID_VERIFICATION_CODE = "INVALID_VERIFICATION_CODE";
this.ERROR_MISSING_CONTENT_LENGTH_HEADER = "MISSING_CONTENT_LENGTH_HEADER";
this.ERROR_NO_RETRIES_LEFT = "NO_RETRIES_LEFT";
this.ERROR_OFFLINE = "OFFLINE";
this.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE";
this.ERROR_SERVICE_TEMPORARILY_UNAVAILABLE = "SERVICE_TEMPORARILY_UNAVAILABLE";
this.ERROR_TOO_MANY_REQUESTS_MSISDN = "TOO_MANY_REQUESTS_MSISDN";
this.ERROR_TOO_MANY_REQUESTS_UNSPECIFIED = "TOO_MANY_REQUESTS_UNSPECIFIED";
this.ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE = "TOO_MANY_REQUESTS_VERIFICATION_CODE";
this.ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD = "TOO_MANY_REQUESTS_VERIFICATION_METHOD";
this.ERROR_UNKNOWN = "UNKNOWN";
this.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
this.ERROR_VERIFICATION_CODE_TIMEOUT = "VERIFICATION_CODE_TIMEOUT";
// Server errno.
// From https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md#response-format
this.ERRNO_UNVERIFIED_ACCOUNT = 104;
this.ERRNO_INVALID_VERIFICATION_CODE = 105;
this.ERRNO_INVALID_BODY_JSON = 106;
this.ERRNO_INVALID_BODY_INVALID_PARAMS = 107;
this.ERRNO_INVALID_BODY_MISSING_PARAMS = 108;
this.ERRNO_INVALID_REQUEST_SIGNATURE = 109;
this.ERRNO_INVALID_AUTH_TOKEN = 110;
this.ERRNO_ENDPOINT_NOT_SUPPORTED = 111;
this.ERRNO_MISSING_CONTENT_LENGTH_HEADER = 112;
this.ERRNO_REQUEST_BODY_TOO_LARGE = 113;
this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE = 114;
this.ERRNO_TOO_MANY_REQUESTS_MSISDN = 115;
this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD = 116;
this.ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED = 117;
this.ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE = 201;
this.ERRNO_UNKNOWN_ERROR = 999;
// Error matching.
this.SERVER_ERRNO_TO_ERROR = {};
SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_JSON] = ERROR_INVALID_BODY_JSON;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_INVALID_PARAMS] = ERROR_INVALID_BODY_PARAMS;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_MISSING_PARAMS] = ERROR_INVALID_BODY_MISSING_PARAMS;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE] = ERROR_INVALID_REQUEST_SIGNATURE;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN] = ERROR_INVALID_AUTH_TOKEN;
SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NOT_SUPPORTED] = ERROR_ENDPOINT_NOT_SUPPORTED;
SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH_HEADER] = ERROR_MISSING_CONTENT_LENGTH_HEADER;
SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE] = ERROR_REQUEST_BODY_TOO_LARGE;
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE] = ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_MSISDN] = ERROR_TOO_MANY_REQUESTS_MSISDN;;
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD] = ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD;;
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED] = ERROR_TOO_MANY_REQUESTS_UNSPECIFIED;;
SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE] = ERROR_SERVICE_TEMPORARILY_UNAVAILABLE;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
// Allow this file to be imported via Components.utils.import().
this.EXPORTED_SYMBOLS = Object.keys(this);

View File

@ -0,0 +1,173 @@
/* 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 = ["MobileIdentityCredentialsStore"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
const CREDENTIALS_DB_NAME = "mobile-id-credentials";
const CREDENTIALS_DB_VERSION = 1;
const CREDENTIALS_STORE_NAME = "credentials-store";
this.MobileIdentityCredentialsStore = function() {
};
this.MobileIdentityCredentialsStore.prototype = {
__proto__: IndexedDBHelper.prototype,
init: function() {
log.debug("MobileIdentityCredentialsStore init");
this.initDBHelper(CREDENTIALS_DB_NAME,
CREDENTIALS_DB_VERSION,
[CREDENTIALS_STORE_NAME]);
},
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
log.debug("upgradeSchema");
/**
* We will be storing objects like:
* {
* msisdn: <string> (key),
* iccId: <string> (index),
* origin: <array> (index),
* msisdnSessionToken: <string>
* }
*/
let objectStore = aDb.createObjectStore(CREDENTIALS_STORE_NAME, {
keyPath: "msisdn"
});
objectStore.createIndex("iccId", "iccId", { unique: true });
objectStore.createIndex("origin", "origin", { unique: true, multiEntry: true });
},
add: function(aIccId, aMsisdn, aOrigin, aSessionToken) {
log.debug("put " + aIccId + ", " + aMsisdn + ", " + aOrigin + ", " +
aSessionToken);
if (!aOrigin || !aSessionToken) {
return Promise.reject(ERROR_INTERNAL_DB_ERROR);
}
let deferred = Promise.defer();
// We first try get an existing record for the given MSISDN.
this.newTxn(
"readwrite",
CREDENTIALS_STORE_NAME,
(aTxn, aStore) => {
let range = IDBKeyRange.only(aMsisdn);
let cursorReq = aStore.openCursor(range);
cursorReq.onsuccess = function(aEvent) {
let cursor = aEvent.target.result;
let record;
// If we already have a record of this MSISDN, we add the origin to
// the list of allowed origins.
if (cursor && cursor.value) {
record = cursor.value;
if (record.origin.indexOf(aOrigin) == -1) {
record.origin.push(aOrigin);
}
cursor.update(record);
} else {
// Otherwise, we store a new record.
record = {
iccId: aIccId,
msisdn: aMsisdn,
origin: [aOrigin],
sessionToken: aSessionToken
};
aStore.add(record);
}
deferred.resolve();
};
cursorReq.onerror = function(aEvent) {
log.error(aEvent.target.error);
deferred.reject(ERROR_INTERNAL_DB_ERROR);
};
}, null, deferred.reject);
return deferred.promise;
},
getByMsisdn: function(aMsisdn) {
log.debug("getByMsisdn " + aMsisdn);
if (!aMsisdn) {
return Promise.resolve(null);
}
let deferred = Promise.defer();
this.newTxn(
"readonly",
CREDENTIALS_STORE_NAME,
(aTxn, aStore) => {
aStore.get(aMsisdn).onsuccess = function(aEvent) {
aTxn.result = aEvent.target.result;
};
},
function(result) {
deferred.resolve(result);
},
deferred.reject
);
return deferred.promise;
},
getByIndex: function(aIndex, aValue) {
log.debug("getByIndex " + aIndex + ", " + aValue);
if (!aValue || !aIndex) {
return Promise.resolve(null);
}
let deferred = Promise.defer();
this.newTxn(
"readonly",
CREDENTIALS_STORE_NAME,
(aTxn, aStore) => {
let index = aStore.index(aIndex);
index.get(aValue).onsuccess = function(aEvent) {
aTxn.result = aEvent.target.result;
};
},
function(result) {
deferred.resolve(result);
},
deferred.reject
);
return deferred.promise;
},
getByOrigin: function(aOrigin) {
return this.getByIndex("origin", aOrigin);
},
getByIccId: function(aIccId) {
return this.getByIndex("iccId", aIccId);
},
delete: function(aMsisdn) {
log.debug("delete " + aMsisdn);
if (!aMsisdn) {
return Promise.resolve();
}
let deferred = Promise.defer();
this.newTxn(
"readwrite",
CREDENTIALS_STORE_NAME,
(aTxn, aStore) => {
aStore.delete(aMsisdn);
},
deferred.resolve,
deferred.reject
);
return deferred.promise;
}
};

View File

@ -0,0 +1,824 @@
/* 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 = [];
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");
#endif
let MobileIdentityManager = {
init: function() {
log.debug("MobileIdentityManager init");
Services.obs.addObserver(this, "xpcom-shutdown", false);
ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
this.messageManagers = {};
// TODO: Store keyPairs and certificates in disk. Bug 1021605.
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.msisdn, msg.prompt);
},
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() {
#ifdef MOZ_B2G_RIL
if (this._iccInfo) {
return this._iccInfo;
}
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 operator = null;
if (rilContext.voice.network &&
rilContext.voice.network.shortName &&
rilContext.voice.network.shortName.length) {
operator = rilContext.voice.network.shortName;
} else if (rilContext.data.network &&
rilContext.data.network.shortName &&
rilContext.data.network.shortName.length) {
operator = rilContext.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: rilContext.voice.roaming
});
}
return this._iccInfo;
#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) {
return Promise.resolve();
}
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) {
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) => {
this.certificates[aSessionToken] = {
cert: signedCert.cert,
validUntil: validUntil
};
deferred.resolve(signedCert.cert);
},
deferred.reject
);
return deferred.promise;
},
/*********************************************************
* 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();
},
/*********************************************************
* Permissions helpers
********************************************************/
hasPermission: function(aPrincipal) {
let permission = permissionManager.testPermissionFromPrincipal(aPrincipal,
MOBILEID_PERM);
return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
},
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(
aOrigin,
aToVerify.msisdn,
aToVerify.iccId,
aToVerify.serviceId === undefined, // external: the phone number does
// not seem to belong to any of the
// device SIM cards.
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(
aOrigin,
aToVerify.serviceId,
aToVerify.iccId,
aToVerify.verificationDetails.mtSender,
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.verificationMethod =
this.iccInfo[serviceId].verificationMethods[0];
toVerify.verificationDetails =
this.iccInfo[serviceId].verificationDetails[toVerify.verificationMethod];
return this._verificationFlow(toVerify, aOrigin);
} else {
toVerify.msisdn = aUserSelection.msisdn;
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) => {
if (!result ||
(!result.phoneNumber && !result.serviceId)) {
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.
if (result.serviceId) {
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
null, // 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);
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);
return creds;
}
// Otherwise, we need to verify the new number selected by the
// user.
return this.verificationFlow(promptResult, aPrincipal.origin);
}
);
}
);
},
/*********************************************************
* 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)
.then(
() => {
deferred.resolve(assertion);
}
);
});
}, deferred.reject
);
}
);
return deferred.promise;
},
getMobileIdAssertion: function(aPrincipal, aPromiseId) {
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);
// 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.
return 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;
}
// It is possible that the ICC associated with the stored
// credentials is not present in the device anymore, so we ask the
// user if she still wants to use it anyway or if she prefers to use
// another phone number.
// If the credentials are associated with an external SIM or there is
// no SIM in the device, we just return the credentials.
if (this.iccInfo && creds.iccId) {
for (let i = 0; i < this.iccInfo.length; i++) {
if (this.iccInfo[i].iccId == creds.iccId) {
return creds;
}
}
// At this point we know that the SIM associated with the credentials
// is not present in the device any more, so we need to ask the user
// what to do.
return this.promptAndVerify(principal, manifestURL, creds);
}
return creds;
}
)
.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 (this.hasPermission(principal)) {
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);
let mm = this.messageManagers[aPromiseId];
mm.sendAsyncMessage("MobileId:GetAssertion:Return:OK", {
promiseId: aPromiseId,
result: assertion
});
}
)
.then(
null,
(error) => {
log.error("getMobileIdAssertion rejected with " + error);
// Notify the error to the UI.
this.ui.error(error);
let mm = this.messageManagers[aPromiseId];
mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
promiseId: aPromiseId,
error: error
});
}
);
},
};
MobileIdentityManager.init();

View File

@ -0,0 +1,89 @@
/* 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 = ["MobileIdentitySmsMoMtVerificationFlow"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "smsService",
"@mozilla.org/sms/smsservice;1",
"nsISmsService");
// In order to send messages through nsISmsService, we need to implement
// nsIMobileMessageCallback, as the WebSMS API implementation is not usable
// from JS.
function SilentSmsRequest(aDeferred) {
this.deferred = aDeferred;
}
SilentSmsRequest.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]),
classID: Components.ID("{ff46f1a8-e040-4ff4-98a7-d5a5b86a2c3e}"),
notifyMessageSent: function notifyMessageSent(aMessage) {
log.debug("Silent message successfully sent");
this.deferred.resolve(aMessage);
},
notifySendMessageFailed: function notifySendMessageFailed(aError) {
log.error("Error sending silent message " + aError);
this.deferred.reject(aError);
}
};
this.MobileIdentitySmsMoMtVerificationFlow = function(aOrigin,
aServiceId,
aIccId,
aMtSender,
aMoVerifier,
aUI,
aClient) {
log.debug("MobileIdentitySmsMoMtVerificationFlow");
MobileIdentitySmsVerificationFlow.call(this,
aOrigin,
null, //msisdn
aIccId,
aServiceId,
false, // external
aMtSender,
aMoVerifier,
aUI,
aClient,
this.smsVerifyStrategy);
};
this.MobileIdentitySmsMoMtVerificationFlow.prototype = {
__proto__: MobileIdentitySmsVerificationFlow.prototype,
smsVerifyStrategy: function() {
// In the MO+MT flow we need to send an SMS to the given moVerifier number
// so the server can find out our phone number to send an SMS back with a
// verification code.
let deferred = Promise.defer();
let silentSmsRequest = new SilentSmsRequest(deferred);
// The MO SMS body that the server expects contains the API endpoint for
// the MO verify request and the HAWK ID parameter derived via HKDF from
// the session token. These parameters should go unnamed and space limited.
let body = SMS_MO_MT_VERIFY + " " +
this.client._deriveHawkCredentials(this.sessionToken).id;
smsService.send(this.verificationOptions.serviceId,
this.verificationOptions.moVerifier,
body,
true, // silent
silentSmsRequest);
log.debug("Sending " + body + " to " + this.verificationOptions.moVerifier);
return deferred.promise;
}
};

View File

@ -0,0 +1,49 @@
/* 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 = ["MobileIdentitySmsMtVerificationFlow"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.MobileIdentitySmsMtVerificationFlow = function(aOrigin,
aMsisdn,
aIccId,
aExternal,
aMtSender,
aUI,
aClient) {
log.debug("MobileIdentitySmsVerificationFlow " + aMsisdn + ", external: " +
aExternal);
MobileIdentitySmsVerificationFlow.call(this,
aOrigin,
aMsisdn,
aIccId,
null, // service ID
aExternal,
aMtSender,
null, // moVerifier
aUI,
aClient,
this.smsVerifyStrategy);
};
this.MobileIdentitySmsMtVerificationFlow.prototype = {
__proto__: MobileIdentitySmsVerificationFlow.prototype,
smsVerifyStrategy: function() {
return this.client.smsMtVerify(this.sessionToken,
this.verificationOptions.msisdn,
this.verificationOptions.external);
}
};

View File

@ -0,0 +1,120 @@
/* 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 = ["MobileIdentitySmsVerificationFlow"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/MobileIdentityVerificationFlow.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
#ifdef MOZ_B2G_RIL
XPCOMUtils.defineLazyServiceGetter(this, "smsService",
"@mozilla.org/sms/smsservice;1",
"nsISmsService");
#endif
this.MobileIdentitySmsVerificationFlow = function(aOrigin,
aMsisdn,
aIccId,
aServiceId,
aExternal,
aMtSender,
aMoVerifier,
aUI,
aClient,
aVerifyStrategy) {
// SMS MT or SMS MO+MT specific verify strategy.
this.smsVerifyStrategy = aVerifyStrategy;
MobileIdentityVerificationFlow.call(this, {
origin: aOrigin,
msisdn: aMsisdn,
iccId: aIccId,
serviceId: aServiceId,
external: aExternal,
mtSender: aMtSender,
moVerifier: aMoVerifier
}, aUI, aClient, this._verifyStrategy, this._cleanupStrategy);
};
this.MobileIdentitySmsVerificationFlow.prototype = {
__proto__: MobileIdentityVerificationFlow.prototype,
observedSilentNumber: null,
onSilentSms: null,
_verifyStrategy: function() {
if (!this.smsVerifyStrategy) {
return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
}
// Even if the user selection is given to us as a possible external phone
// number, it is also possible that the phone number introduced by the
// user belongs to one of the SIMs inserted in the device which MSISDN
// is unknown for us, so we always observe for incoming messages coming
// from the given mtSender.
#ifdef MOZ_B2G_RIL
this.observedSilentNumber = this.verificationOptions.mtSender;
try {
smsService.addSilentNumber(this.observedSilentNumber);
} catch (e) {
log.warn("We are already listening for that number");
}
this.onSilentSms = (function(aSubject, aTopic, aData) {
log.debug("Got silent message " + aSubject.sender + " - " + aSubject.body);
// We might have observed a notification of an incoming silent message
// for other number. In that case, we just bail out.
if (aSubject.sender != this.observedSilentNumber) {
return;
}
// We got the SMS containing the verification code.
// If the phone number we are trying to verify is or can be an external
// phone number (meaning that it doesn't belong to any of the inserted
// SIMs) we will be receiving an human readable SMS containing a short
// verification code. In this case we need to parse the SMS body to
// extract the verification code.
// Otherwise, we just use the whole SMS body as it should contain a long
// verification code.
let verificationCode = aSubject.body;
if (this.verificationOptions.external) {
// We just take the numerical characters from the body.
verificationCode = aSubject.body.replace(/[^0-9]/g,'');
}
log.debug("Verification code: " + verificationCode);
this.verificationCodeDeferred.resolve(verificationCode);
}).bind(this);
Services.obs.addObserver(this.onSilentSms,
SILENT_SMS_RECEIVED_TOPIC,
false);
log.debug("Observing messages from " + this.observedSilentNumber);
#endif
return this.smsVerifyStrategy();
},
_cleanupStrategy: function() {
#ifdef MOZ_B2G_RIL
smsService.removeSilentNumber(this.observedSilentNumber);
Services.obs.removeObserver(this.onSilentSms,
SILENT_SMS_RECEIVED_TOPIC);
this.observedSilentNumber = null;
this.onSilentSms = null;
#endif
}
};

View File

@ -0,0 +1,31 @@
/* 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 = ["MobileIdentityUIGluePhoneInfo",
"MobileIdentityUIGluePromptResult"];
this.MobileIdentityUIGluePhoneInfo = function (aMsisdn, aOperator, aServiceId,
aExternal, aPrimary) {
this.msisdn = aMsisdn;
this.operator = aOperator;
this.serviceId = aServiceId;
// A phone number is considered "external" when it doesn't or we don't know
// if it does belong to any of the device SIM cards.
this.external = aExternal;
this.primary = aPrimary;
}
this.MobileIdentityUIGluePhoneInfo.prototype = {};
this.MobileIdentityUIGluePromptResult = function (aPhoneNumber, aPrefix, aMcc,
aServiceId) {
this.phoneNumber = aPhoneNumber;
this.prefix = aPrefix;
this.mcc = aMcc;
this.serviceId = aServiceId;
}
this.MobileIdentityUIGluePromptResult.prototype = {};

View File

@ -0,0 +1,213 @@
/* 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 = ["MobileIdentityVerificationFlow"];
const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.MobileIdentityVerificationFlow = function(aVerificationOptions,
aUI,
aClient,
aVerifyStrategy,
aCleanupStrategy) {
this.verificationOptions = aVerificationOptions;
this.ui = aUI;
this.client = aClient;
this.retries = VERIFICATIONCODE_RETRIES;
this.verifyStrategy = aVerifyStrategy;
this.cleanupStrategy = aCleanupStrategy;
};
MobileIdentityVerificationFlow.prototype = {
doVerification: function() {
log.debug("Start verification flow");
return this.register()
.then(
(registerResult) => {
log.debug("Register result ${}", registerResult);
if (!registerResult || !registerResult.msisdnSessionToken) {
return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
}
this.sessionToken = registerResult.msisdnSessionToken;
return this._doVerification();
}
)
},
_doVerification: function() {
log.debug("_doVerification");
// We save the timestamp of the start of the verification timeout to be
// able to provide to the UI the remaining time on each retry.
if (!this.timer) {
log.debug("Creating verification code timer");
this.timerCreation = Date.now();
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
VERIFICATIONCODE_TIMEOUT,
this.timer.TYPE_ONE_SHOT);
}
if (!this.verifyStrategy) {
return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
}
this.verificationCodeDeferred = Promise.defer();
this.verifyStrategy()
.then(
() => {
// If the verification flow can be for an external phone number,
// we need to ask the user for the verification code.
// In that case we don't do a notification about the verification
// process being done until the user enters the verification code
// in the UI.
if (this.verificationOptions.external) {
let timeLeft = 0;
if (this.timer) {
timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
Date.now();
}
this.ui.verificationCodePrompt(this.retries,
VERIFICATIONCODE_TIMEOUT / 1000,
timeLeft / 1000)
.then(
(verificationCode) => {
if (!verificationCode) {
return this.verificationCodeDeferred.reject(
ERROR_INTERNAL_INVALID_PROMPT_RESULT);
}
// If the user got the verification code that means that the
// introduced phone number didn't belong to any of the inserted
// SIMs.
this.ui.verify();
this.verificationCodeDeferred.resolve(verificationCode);
}
);
} else {
this.ui.verify();
}
},
(reason) => {
this.verificationCodeDeferred.reject(reason);
}
);
return this.verificationCodeDeferred.promise.then(
this.onVerificationCode.bind(this)
);
},
// When we receive a verification code from the UI, we check it against
// the server. If the verification code is incorrect, we decrease the
// number of retries left and allow the user to try again. If there is no
// possible retry left, we notify about this error so the UI can allow the
// user to request the resend of a new verification code.
onVerificationCode: function(aVerificationCode) {
log.debug("onVerificationCode " + aVerificationCode);
if (!aVerificationCode) {
this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
return this._doVerification();
}
// Before checking the verification code against the server we set the
// "verifying" flag to queue timeout expiration events received before
// the server request is completed. If the server request is positive
// we will discard the timeout event, otherwise we will progress the
// event to the UI to allow the user to retry.
this.verifying = true;
return this.verifyCode(aVerificationCode)
.then(
(result) => {
if (!result) {
return Promise.reject(INTERNAL_UNEXPECTED);
}
// The code was correct!
// At this point the phone number is verified.
// We return the given verification options with the session token
// to be stored in the credentials store. With this data we will be
// asking the server to give us a certificate to generate assertions.
this.verificationOptions.sessionToken = this.sessionToken;
this.verificationOptions.msisdn = result.msisdn ||
this.verificationOptions.msisdn;
return this.verificationOptions;
},
(error) => {
log.error("Verification code error " + error);
this.retries--;
log.error("Retries left " + this.retries);
if (!this.retries) {
this.ui.error(ERROR_NO_RETRIES_LEFT);
return Promise.reject(ERROR_NO_RETRIES_LEFT);
}
this.verifying = false;
if (this.queuedTimeout) {
this.onVerificationCodeTimeout();
}
return this._doVerification();
}
);
},
onVerificationCodeTimeout: function() {
// It is possible that we get the timeout when we are checking a
// verification code with the server. In that case, we queue the
// timeout to be triggered after we receive the reply from the server
// if needed.
if (this.verifying) {
this.queuedTimeout = true;
return;
}
// When the verification process times out we do a clean up, reject
// the corresponding promise and notify the UI about the timeout.
if (this.verificationCodeDeferred) {
this.verificationCodeDeferred.reject(ERROR_VERIFICATION_CODE_TIMEOUT);
}
this.ui.error(ERROR_VERIFICATION_CODE_TIMEOUT);
},
register: function() {
return this.client.register();
},
verifyCode: function(aVerificationCode) {
return this.client.verifyCode(this.sessionToken, aVerificationCode);
},
unregister: function() {
return this.client.unregister(this.sessionToken);
},
cleanup: function(aUnregister = false) {
log.debug("Verification flow cleanup");
this.queuedTimeout = false;
this.retries = VERIFICATIONCODE_RETRIES;
if (this.timer) {
this.timer.cancel();
this.timer = null;
}
if (aUnregister) {
this.unregister().
then(
() => {
this.sessionToken = null;
}
);
}
if (this.cleanupStrategy) {
this.cleanupStrategy();
}
}
};

View File

@ -0,0 +1,11 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPIDL_SOURCES += [
'nsIMobileIdentityUIGlue.idl'
]
XPIDL_MODULE = 'services_mobileidentity'

View File

@ -0,0 +1,77 @@
/* 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/. */
#include "nsISupports.idl"
[scriptable, uuid(6c4c5758-e041-4e0d-98da-67bb552f8018)]
interface nsIMobileIdentityUIGlue : nsISupports
{
/**
* Request the creation of a Mobile ID UI flow.
*
* The permission prompt starts the verification flow asking the user
* for permission to share her phone number and allowing her to choose
* an already known phone number, a SIM which phone number is unknown
* (even in a multi-SIM scenario) or an external phone number.
* Selecting a phone number implies giving permission to share it with the
* API caller, so the UI should be clear about this.
*
* @manifestURL manifest URL of the mobile ID requester.
* @iccInfo array of objects containing the information about the
* SIM cards available in the device and that can be used for the
* phone number verification and share process.
*
* Returns a Promise. An instance of nsIMobileIdentityUIGluePromptResult will
* be returned as result of the Promise or a single string containing an error
* in case of rejection.
*/
jsval startFlow(in DOMString manifestURL, in jsval iccInfo);
/**
* Will prompt the user to enter a code used to verify a phone number.
* This will only be called if an external phone number is selected in
* startFlow().
*
* @retries number of retries left to validate a verification code.
* @timeout the verification code expires after the timeout fires. This is
* the total life time of the verification code.
* @timeLeft we might call verificationCodePrompt more than once for the
* same verification flow (i.e. when the verification code entered
* by the user is incorrect) so we give to the UI the amount of
* time left before the verification code expires.
*
* Returns a Promise. The value of the resolved promise will be the
* verification code introduced through the UI or an error in case of
* rejection of the promise.
*/
jsval verificationCodePrompt(in short retries,
in long timeout,
in long timeLeft);
/**
* Notify the UI about the start of the verification process.
*/
void verify();
/**
* Notify the UI about an error in the verification process.
*/
void error(in DOMString error);
/**
* Notify the UI about the succesful phone number verification.
*/
void verified(in DOMString verifiedPhoneNumber);
/**
* Callback to be called when the user cancels the verification flow via UI.
*/
attribute jsval oncancel;
/**
* Callback to be called when the user requests a resend of a verification
* code.
*/
attribute jsval onresendcode;
};

View File

@ -0,0 +1,22 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
PARALLEL_DIRS += ['interfaces']
EXTRA_JS_MODULES += [
'MobileIdentityClient.jsm',
'MobileIdentityCommon.jsm',
'MobileIdentityCredentialsStore.jsm',
'MobileIdentitySmsMoMtVerificationFlow.jsm',
'MobileIdentitySmsMtVerificationFlow.jsm',
'MobileIdentityUIGlueCommon.jsm',
'MobileIdentityVerificationFlow.jsm'
]
EXTRA_PP_JS_MODULES += [
'MobileIdentityManager.jsm',
'MobileIdentitySmsVerificationFlow.jsm'
]

View File

@ -6,7 +6,7 @@
PARALLEL_DIRS += [
'common',
'crypto',
'crypto'
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
@ -27,4 +27,7 @@ if CONFIG['MOZ_SERVICES_FXACCOUNTS']:
if CONFIG['MOZ_SERVICES_SYNC']:
PARALLEL_DIRS += ['sync']
if CONFIG['MOZ_B2G']:
PARALLEL_DIRS += ['mobileid']
SPHINX_TREES['services'] = 'docs'