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:
Philipp von Weitershausen 2010-07-20 00:28:54 +02:00
parent b4834f3185
commit f35d794526
6 changed files with 177 additions and 24 deletions

View File

@ -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;
}
};

View File

@ -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
});

View File

@ -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() {});
}
}

View File

@ -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";

View File

@ -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");

View 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("");
}
}