mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 00:01:50 +00:00
Bug 579510 - Make sure multi-byte passwords are stored and sent correctly [r=mconnor]
UTF8-encode passwords when creating accounts, changing passwords, and when authenticating. Detect old low-byte only passwords on the server and reupload them as UTF8.
This commit is contained in:
parent
b4834f3185
commit
f35d794526
@ -34,7 +34,8 @@
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ['Auth', 'BasicAuthenticator', 'NoOpAuthenticator'];
|
||||
const EXPORTED_SYMBOLS = ['Auth', 'BrokenBasicAuthenticator',
|
||||
'BasicAuthenticator', 'NoOpAuthenticator'];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
@ -55,13 +56,26 @@ NoOpAuthenticator.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
// Warning: This will drop the high unicode bytes from passwords.
|
||||
// Use BasicAuthenticator to send non-ASCII passwords UTF8-encoded.
|
||||
function BrokenBasicAuthenticator(identity) {
|
||||
this._id = identity;
|
||||
}
|
||||
BrokenBasicAuthenticator.prototype = {
|
||||
onRequest: function BasicAuth_onRequest(headers) {
|
||||
headers['Authorization'] = 'Basic ' +
|
||||
btoa(this._id.username + ':' + this._id.password);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
||||
function BasicAuthenticator(identity) {
|
||||
this._id = identity;
|
||||
}
|
||||
BasicAuthenticator.prototype = {
|
||||
onRequest: function BasicAuth_onRequest(headers) {
|
||||
onRequest: function onRequest(headers) {
|
||||
headers['Authorization'] = 'Basic ' +
|
||||
btoa(this._id.username + ':' + this._id.password);
|
||||
btoa(this._id.username + ':' + this._id.passwordUTF8);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
@ -577,6 +577,26 @@ WeaveSvc.prototype = {
|
||||
return true;
|
||||
|
||||
case 401:
|
||||
// Login failed. If the password contains non-ASCII characters,
|
||||
// perhaps the server password is an old low-byte only one?
|
||||
let id = ID.get('WeaveID');
|
||||
if (id.password != id.passwordUTF8) {
|
||||
let res = new Resource(this.infoURL);
|
||||
let auth = new BrokenBasicAuthenticator(id);
|
||||
res.authenticator = auth;
|
||||
test = res.get();
|
||||
if (test.status == 200) {
|
||||
this._log.debug("Non-ASCII password detected. "
|
||||
+ "Changing to UTF-8 version.");
|
||||
// Let's change the password on the server to the UTF8 version.
|
||||
let url = this.userAPI + this.username + "/password";
|
||||
res = new Resource(url);
|
||||
res.authenticator = auth;
|
||||
res.post(id.passwordUTF8);
|
||||
return this.verifyLogin();
|
||||
}
|
||||
}
|
||||
// Yes, we want to fall through to the 404 case.
|
||||
case 404:
|
||||
// Check that we're verifying with the correct cluster
|
||||
if (this._setCluster())
|
||||
@ -636,7 +656,7 @@ WeaveSvc.prototype = {
|
||||
this._notify("changepwd", "", function() {
|
||||
let url = this.userAPI + this.username + "/password";
|
||||
try {
|
||||
let resp = new Resource(url).post(newpass);
|
||||
let resp = new Resource(url).post(Utils.encodeUTF8(newpass));
|
||||
if (resp.status != 200) {
|
||||
this._log.debug("Password change failed: " + resp);
|
||||
return false;
|
||||
@ -830,10 +850,10 @@ WeaveSvc.prototype = {
|
||||
},
|
||||
|
||||
createAccount: function WeaveSvc_createAccount(username, password, email,
|
||||
captchaChallenge, captchaResponse)
|
||||
{
|
||||
captchaChallenge, captchaResponse) {
|
||||
let payload = JSON.stringify({
|
||||
"password": password, "email": email,
|
||||
"password": Utils.encodeUTF8(password),
|
||||
"email": email,
|
||||
"captcha-challenge": captchaChallenge,
|
||||
"captcha-response": captchaResponse
|
||||
});
|
||||
|
@ -7,21 +7,29 @@ Cu.import("resource://services-sync/util.js");
|
||||
let logger;
|
||||
|
||||
function server_handler(metadata, response) {
|
||||
let body;
|
||||
let body, statusCode, status;
|
||||
|
||||
// no btoa() in xpcshell. it's guest:guest
|
||||
if (metadata.hasHeader("Authorization") &&
|
||||
metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
|
||||
body = "This path exists and is protected";
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
|
||||
} else {
|
||||
body = "This path exists and is protected - failed";
|
||||
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
switch (metadata.getHeader("Authorization")) {
|
||||
// guest:guest
|
||||
case "Basic Z3Vlc3Q6Z3Vlc3Q=":
|
||||
body = "This path exists and is protected";
|
||||
statusCode = 200;
|
||||
status = "OK";
|
||||
break;
|
||||
// johndoe:moneyislike$\u20ac\xa5\u5143
|
||||
case "Basic am9obmRvZTptb25leWlzbGlrZSTigqzCpeWFgw==":
|
||||
body = "This path exists and is protected by a UTF8 password";
|
||||
statusCode = 200;
|
||||
status = "OK";
|
||||
break;
|
||||
default:
|
||||
body = "This path exists and is protected - failed";
|
||||
statusCode = 401;
|
||||
status = "Unauthorized";
|
||||
}
|
||||
|
||||
response.setStatusLine(metadata.httpVersion, statusCode, status);
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
@ -31,15 +39,24 @@ function run_test() {
|
||||
|
||||
let server = new nsHttpServer();
|
||||
server.registerPathHandler("/foo", server_handler);
|
||||
server.registerPathHandler("/bar", server_handler);
|
||||
server.start(8080);
|
||||
|
||||
let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
|
||||
let auth2 = new BasicAuthenticator(
|
||||
new Identity("secret2", "johndoe", "moneyislike$\u20ac\xa5\u5143"));
|
||||
Auth.defaultAuthenticator = auth;
|
||||
Auth.registerAuthenticator("bar$", auth2);
|
||||
|
||||
let res = new Resource("http://localhost:8080/foo");
|
||||
let content = res.get();
|
||||
do_check_eq(content, "This path exists and is protected");
|
||||
do_check_eq(content.status, 200);
|
||||
try {
|
||||
let content = new Resource("http://localhost:8080/foo").get();
|
||||
do_check_eq(content, "This path exists and is protected");
|
||||
do_check_eq(content.status, 200);
|
||||
|
||||
server.stop(function() {});
|
||||
content = new Resource("http://localhost:8080/bar").get();
|
||||
do_check_eq(content, "This path exists and is protected by a UTF8 password");
|
||||
do_check_eq(content.status, 200);
|
||||
} finally {
|
||||
server.stop(function() {});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
Cu.import("resource://services-sync/service.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-sync/constants.js");
|
||||
|
||||
function run_test() {
|
||||
@ -33,12 +34,19 @@ function run_test() {
|
||||
res = Weave.Service.changePassword("ILoveJane83");
|
||||
do_check_true(res);
|
||||
do_check_eq(Weave.Service.password, "ILoveJane83");
|
||||
do_check_eq(requestBody, "ILoveJane83");
|
||||
|
||||
_("Make sure the password has been persisted in the login manager.");
|
||||
let logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null,
|
||||
PWDMGR_PASSWORD_REALM);
|
||||
do_check_eq(logins[0].password, "ILoveJane83");
|
||||
|
||||
_("A non-ASCII password is UTF-8 encoded.");
|
||||
res = Weave.Service.changePassword("moneyislike$\u20ac\xa5\u5143");
|
||||
do_check_true(res);
|
||||
do_check_eq(Weave.Service.password, "moneyislike$\u20ac\xa5\u5143");
|
||||
do_check_eq(requestBody, Utils.encodeUTF8("moneyislike$\u20ac\xa5\u5143"));
|
||||
|
||||
_("changePassword() returns false for a server error, the password won't change.");
|
||||
Weave.Svc.Login.removeAllLogins();
|
||||
Weave.Service.username = "janedoe";
|
||||
|
@ -1,3 +1,4 @@
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-sync/service.js");
|
||||
|
||||
function run_test() {
|
||||
@ -33,6 +34,14 @@ function run_test() {
|
||||
do_check_eq(payload["captcha-challenge"], "challenge");
|
||||
do_check_eq(payload["captcha-response"], "response");
|
||||
|
||||
_("A non-ASCII password is UTF-8 encoded.");
|
||||
res = Weave.Service.createAccount("johndoe", "moneyislike$\u20ac\xa5\u5143",
|
||||
"john@doe", "challenge", "response");
|
||||
do_check_eq(res, null);
|
||||
payload = JSON.parse(requestBody);
|
||||
do_check_eq(payload.password,
|
||||
Utils.encodeUTF8("moneyislike$\u20ac\xa5\u5143"));
|
||||
|
||||
_("Invalid captcha or other user-friendly error.");
|
||||
res = Weave.Service.createAccount("janedoe", "anothersecretpw", "jane@doe",
|
||||
"challenge", "response");
|
||||
|
85
services/sync/tests/unit/test_service_passwordUTF8.js
Normal file
85
services/sync/tests/unit/test_service_passwordUTF8.js
Normal file
@ -0,0 +1,85 @@
|
||||
Cu.import("resource://services-sync/auth.js");
|
||||
Cu.import("resource://services-sync/resource.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-sync/service.js");
|
||||
|
||||
const JAPANESE = "\u34ff\u35ff\u36ff\u37ff";
|
||||
const APPLES = "\uf8ff\uf8ff\uf8ff\uf8ff";
|
||||
const LOWBYTES = "\xff\xff\xff\xff";
|
||||
|
||||
// Poor man's /etc/passwd. Static since there's no btoa()/atob() in xpcshell.
|
||||
let basicauth = {};
|
||||
basicauth[LOWBYTES] = "Basic am9obmRvZTr/////";
|
||||
basicauth[Utils.encodeUTF8(JAPANESE)] = "Basic am9obmRvZTrjk7/jl7/jm7/jn78=";
|
||||
|
||||
// Global var for the server password, read by info_collections(),
|
||||
// modified by change_password().
|
||||
let server_password;
|
||||
|
||||
function info_collections(request, response) {
|
||||
let body, statusCode, status;
|
||||
let basic = basicauth[server_password];
|
||||
|
||||
if (basic && (request.getHeader("Authorization") == basic)) {
|
||||
body = "{}";
|
||||
statusCode = 200;
|
||||
status = "OK";
|
||||
} else {
|
||||
statusCode = 401;
|
||||
body = status = "Unauthorized";
|
||||
}
|
||||
response.setStatusLine(request.httpVersion, statusCode, status);
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function change_password(request, response) {
|
||||
let body, statusCode, status;
|
||||
let basic = basicauth[server_password];
|
||||
|
||||
if (basic && (request.getHeader("Authorization") == basic)) {
|
||||
server_password = readBytesFromInputStream(request.bodyInputStream);
|
||||
body = "";
|
||||
statusCode = 200;
|
||||
status = "OK";
|
||||
} else {
|
||||
statusCode = 401;
|
||||
body = status = "Unauthorized";
|
||||
}
|
||||
response.setStatusLine(request.httpVersion, statusCode, status);
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let server = httpd_setup({
|
||||
"/1.0/johndoe/info/collections": info_collections,
|
||||
"/user/1.0/johndoe/password": change_password
|
||||
});
|
||||
|
||||
Weave.Service.username = "johndoe";
|
||||
Weave.Service.password = JAPANESE;
|
||||
Weave.Service.passphrase = "Must exist, but contents irrelevant.";
|
||||
Weave.Service.serverURL = "http://localhost:8080/";
|
||||
|
||||
try {
|
||||
_("Try to log in with the password.");
|
||||
server_password = "foobar";
|
||||
do_check_false(Weave.Service.verifyLogin());
|
||||
do_check_eq(server_password, "foobar");
|
||||
|
||||
_("Make the server password the low byte version of our password. Login should work and have transparently changed the password to the UTF8 version.");
|
||||
server_password = LOWBYTES;
|
||||
do_check_true(Weave.Service.verifyLogin());
|
||||
do_check_eq(server_password, Utils.encodeUTF8(JAPANESE));
|
||||
|
||||
_("Can't use a password that has the same low bytes as ours.");
|
||||
Weave.Service.password = APPLES;
|
||||
do_check_false(Weave.Service.verifyLogin());
|
||||
do_check_eq(server_password, Utils.encodeUTF8(JAPANESE));
|
||||
|
||||
} finally {
|
||||
server.stop(function() {});
|
||||
Weave.Svc.Prefs.resetBranch("");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user