зеркало из https://github.com/mozilla/gecko-dev.git
Bug 943521 - Use onepw prototcol in fxa client. r=ckarlof
This commit is contained in:
Родитель
45a3dcc558
Коммит
7deb4b713b
|
@ -5,6 +5,24 @@
|
|||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
// A wise line of Greek verse, and the utf-8 byte encoding.
|
||||
// N.b., Greek begins at utf-8 ce 91
|
||||
const TEST_STR = "πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα";
|
||||
const TEST_HEX = h("cf 80 cf 8c ce bb ce bb 27 20 ce bf e1 bc b6 ce"+
|
||||
"b4 27 20 e1 bc 80 ce bb cf 8e cf 80 ce b7 ce be"+
|
||||
"2c 20 e1 bc 80 ce bb ce bb 27 20 e1 bc 90 cf 87"+
|
||||
"e1 bf 96 ce bd ce bf cf 82 20 e1 bc 93 ce bd 20"+
|
||||
"ce bc ce ad ce b3 ce b1");
|
||||
// Integer byte values for the above
|
||||
const TEST_BYTES = [207,128,207,140,206,187,206,187,
|
||||
39, 32,206,191,225,188,182,206,
|
||||
180, 39, 32,225,188,128,206,187,
|
||||
207,142,207,128,206,183,206,190,
|
||||
44, 32,225,188,128,206,187,206,
|
||||
187, 39, 32,225,188,144,207,135,
|
||||
225,191,150,206,189,206,191,207,
|
||||
130, 32,225,188,147,206,189, 32,
|
||||
206,188,206,173,206,179,206,177];
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
|
@ -53,3 +71,70 @@ add_test(function test_bad_argument() {
|
|||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringAsHex() {
|
||||
do_check_eq(TEST_HEX, CommonUtils.stringAsHex(TEST_STR));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexAsString() {
|
||||
do_check_eq(TEST_STR, CommonUtils.hexAsString(TEST_HEX));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexToBytes() {
|
||||
let bytes = CommonUtils.hexToBytes(TEST_HEX);
|
||||
do_check_eq(TEST_BYTES.length, bytes.length);
|
||||
// Ensure that the decimal values of each byte are correct
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(bytes)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_bytesToHex() {
|
||||
// Create a list of our character bytes from the reference int values
|
||||
let bytes = CommonUtils.byteArrayToString(TEST_BYTES);
|
||||
do_check_eq(TEST_HEX, CommonUtils.bytesAsHex(bytes));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringToBytes() {
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(CommonUtils.stringToBytes(TEST_STR))));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringRoundTrip() {
|
||||
do_check_eq(TEST_STR,
|
||||
CommonUtils.hexAsString(CommonUtils.stringAsHex(TEST_STR)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexRoundTrip() {
|
||||
do_check_eq(TEST_HEX,
|
||||
CommonUtils.stringAsHex(CommonUtils.hexAsString(TEST_HEX)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_byteArrayRoundTrip() {
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(CommonUtils.byteArrayToString(TEST_BYTES))));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function arraysEqual(a1, a2) {
|
||||
if (a1.length !== a2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a1.length; i++) {
|
||||
if (a1[i] !== a2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -196,12 +196,21 @@ this.CommonUtils = {
|
|||
return [String.fromCharCode(byte) for each (byte in bytes)].join("");
|
||||
},
|
||||
|
||||
stringToByteArray: function stringToByteArray(bytesString) {
|
||||
return [String.charCodeAt(byte) for each (byte in bytesString)];
|
||||
},
|
||||
|
||||
bytesAsHex: function bytesAsHex(bytes) {
|
||||
let hex = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2);
|
||||
}
|
||||
return hex;
|
||||
return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2)
|
||||
for (byte in bytes)].join("");
|
||||
},
|
||||
|
||||
stringAsHex: function stringAsHex(str) {
|
||||
return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
|
||||
},
|
||||
|
||||
stringToBytes: function stringToBytes(str) {
|
||||
return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
|
||||
},
|
||||
|
||||
hexToBytes: function hexToBytes(str) {
|
||||
|
@ -212,6 +221,10 @@ this.CommonUtils = {
|
|||
return String.fromCharCode.apply(String, bytes);
|
||||
},
|
||||
|
||||
hexAsString: function hexAsString(hex) {
|
||||
return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
|
||||
},
|
||||
|
||||
/**
|
||||
* Base32 encode (RFC 4648) a string
|
||||
*/
|
||||
|
|
|
@ -167,20 +167,24 @@ this.CryptoUtils = {
|
|||
* c: the number of iterations, a positive integer: e.g., 4096
|
||||
* dkLen: the length in octets of the destination
|
||||
* key, a positive integer: e.g., 16
|
||||
* hmacAlg: The algorithm to use for hmac
|
||||
* hmacLen: The hmac length
|
||||
*
|
||||
* The default value of 20 for hmacLen is appropriate for SHA1. For SHA256,
|
||||
* hmacLen should be 32.
|
||||
*
|
||||
* The output is an octet string of length dkLen, which you
|
||||
* can encode as you wish.
|
||||
*/
|
||||
pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen) {
|
||||
pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen,
|
||||
hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) {
|
||||
|
||||
// We don't have a default in the algo itself, as NSS does.
|
||||
// Use the constant.
|
||||
if (!dkLen) {
|
||||
dkLen = SYNC_KEY_DECODED_LENGTH;
|
||||
}
|
||||
|
||||
/* For HMAC-SHA-1 */
|
||||
const HLEN = 20;
|
||||
|
||||
function F(S, c, i, h) {
|
||||
|
||||
function XOR(a, b, isA) {
|
||||
|
@ -216,27 +220,27 @@ this.CryptoUtils = {
|
|||
}
|
||||
|
||||
ret = U[0];
|
||||
for (j = 1; j < c; j++) {
|
||||
for (let j = 1; j < c; j++) {
|
||||
ret = CommonUtils.byteArrayToString(XOR(ret, U[j]));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
let l = Math.ceil(dkLen / HLEN);
|
||||
let r = dkLen - ((l - 1) * HLEN);
|
||||
let l = Math.ceil(dkLen / hmacLen);
|
||||
let r = dkLen - ((l - 1) * hmacLen);
|
||||
|
||||
// Reuse the key and the hasher. Remaking them 4096 times is 'spensive.
|
||||
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1,
|
||||
let h = CryptoUtils.makeHMACHasher(hmacAlg,
|
||||
CryptoUtils.makeHMACKey(P));
|
||||
|
||||
T = [];
|
||||
let T = [];
|
||||
for (let i = 0; i < l;) {
|
||||
T[i] = F(S, c, ++i, h);
|
||||
}
|
||||
|
||||
let ret = "";
|
||||
for (i = 0; i < l-1;) {
|
||||
for (let i = 0; i < l-1;) {
|
||||
ret += T[i++];
|
||||
}
|
||||
ret += T[l - 1].substr(0, r);
|
||||
|
|
|
@ -1,15 +1,166 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Evil.
|
||||
let btoa = Cu.import("resource://services-common/utils.js").btoa;
|
||||
// XXX until bug 937114 is fixed
|
||||
Cu.importGlobalProperties(['btoa']);
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
let {bytesAsHex: b2h} = CommonUtils;
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_pbkdf2() {
|
||||
let symmKey16 = CryptoUtils.pbkdf2Generate("secret phrase", "DNXPzPpiwn", 4096, 16);
|
||||
do_check_eq(symmKey16.length, 16);
|
||||
do_check_eq(btoa(symmKey16), "d2zG0d2cBfXnRwMUGyMwyg==");
|
||||
do_check_eq(CommonUtils.encodeBase32(symmKey16), "O5WMNUO5TQC7LZ2HAMKBWIZQZI======");
|
||||
let symmKey32 = CryptoUtils.pbkdf2Generate("passphrase", "salt", 4096, 32);
|
||||
do_check_eq(symmKey32.length, 32);
|
||||
});
|
||||
|
||||
// http://tools.ietf.org/html/rfc6070
|
||||
// PBKDF2 HMAC-SHA1 Test Vectors
|
||||
add_task(function test_pbkdf2_hmac_sha1() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let vectors = [
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 1,
|
||||
dkLen: 20,
|
||||
DK: h("0c 60 c8 0f 96 1f 0e 71"+
|
||||
"f3 a9 b5 24 af 60 12 06"+
|
||||
"2f e0 37 a6"), // (20 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 2,
|
||||
dkLen: 20,
|
||||
DK: h("ea 6c 01 4d c7 2d 6f 8c"+
|
||||
"cd 1e d9 2a ce 1d 41 f0"+
|
||||
"d8 de 89 57"), // (20 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 4096,
|
||||
dkLen: 20,
|
||||
DK: h("4b 00 79 01 b7 65 48 9a"+
|
||||
"be ad 49 d9 26 f7 21 d0"+
|
||||
"65 a4 29 c1"), // (20 octets)
|
||||
},
|
||||
|
||||
// XXX Uncomment the following test after Bug 968567 lands
|
||||
//
|
||||
// XXX As it stands, I estimate that the CryptoUtils implementation will
|
||||
// take approximately 16 hours in my 2.3GHz MacBook to perform this many
|
||||
// rounds.
|
||||
//
|
||||
// {P: "password", // (8 octets)
|
||||
// S: "salt" // (4 octets)
|
||||
// c: 16777216,
|
||||
// dkLen = 20,
|
||||
// DK: h("ee fe 3d 61 cd 4d a4 e4"+
|
||||
// "e9 94 5b 3d 6b a2 15 8c"+
|
||||
// "26 34 e9 84"), // (20 octets)
|
||||
// },
|
||||
|
||||
{P: "passwordPASSWORDpassword", // (24 octets)
|
||||
S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets)
|
||||
c: 4096,
|
||||
dkLen: 25,
|
||||
DK: h("3d 2e ec 4f e4 1c 84 9b"+
|
||||
"80 c8 d8 36 62 c0 e4 4a"+
|
||||
"8b 29 1a 96 4c f2 f0 70"+
|
||||
"38"), // (25 octets)
|
||||
|
||||
},
|
||||
|
||||
{P: "pass\0word", // (9 octets)
|
||||
S: "sa\0lt", // (5 octets)
|
||||
c: 4096,
|
||||
dkLen: 16,
|
||||
DK: h("56 fa 6a a7 55 48 09 9d"+
|
||||
"cc 37 d7 f0 34 25 e0 c3"), // (16 octets)
|
||||
},
|
||||
];
|
||||
|
||||
for (let v of vectors) {
|
||||
do_check_eq(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen)));
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// I can't find any normative ietf test vectors for pbkdf2 hmac-sha256.
|
||||
// The following vectors are derived with the same inputs as above (the sha1
|
||||
// test). Results verified by users here:
|
||||
// https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors
|
||||
add_task(function test_pbkdf2_hmac_sha256() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let vectors = [
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 1,
|
||||
dkLen: 32,
|
||||
DK: h("12 0f b6 cf fc f8 b3 2c"+
|
||||
"43 e7 22 52 56 c4 f8 37"+
|
||||
"a8 65 48 c9 2c cc 35 48"+
|
||||
"08 05 98 7c b7 0b e1 7b"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 2,
|
||||
dkLen: 32,
|
||||
DK: h("ae 4d 0c 95 af 6b 46 d3"+
|
||||
"2d 0a df f9 28 f0 6d d0"+
|
||||
"2a 30 3f 8e f3 c2 51 df"+
|
||||
"d6 e2 d8 5a 95 47 4c 43"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 4096,
|
||||
dkLen: 32,
|
||||
DK: h("c5 e4 78 d5 92 88 c8 41"+
|
||||
"aa 53 0d b6 84 5c 4c 8d"+
|
||||
"96 28 93 a0 01 ce 4e 11"+
|
||||
"a4 96 38 73 aa 98 13 4a"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "passwordPASSWORDpassword", // (24 octets)
|
||||
S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets)
|
||||
c: 4096,
|
||||
dkLen: 40,
|
||||
DK: h("34 8c 89 db cb d3 2b 2f"+
|
||||
"32 d8 14 b8 11 6e 84 cf"+
|
||||
"2b 17 34 7e bc 18 00 18"+
|
||||
"1c 4e 2a 1f b8 dd 53 e1"+
|
||||
"c6 35 51 8c 7d ac 47 e9"), // (40 octets)
|
||||
},
|
||||
|
||||
{P: "pass\0word", // (9 octets)
|
||||
S: "sa\0lt", // (5 octets)
|
||||
c: 4096,
|
||||
dkLen: 16,
|
||||
DK: h("89 b6 9d 05 16 f8 29 89"+
|
||||
"3c 69 62 26 65 0a 86 87"), // (16 octets)
|
||||
},
|
||||
];
|
||||
|
||||
for (let v of vectors) {
|
||||
do_check_eq(v.DK,
|
||||
b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32)));
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This module implements client-side key stretching for use in Firefox
|
||||
* Accounts account creation and login.
|
||||
*
|
||||
* See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Credentials"];
|
||||
|
||||
const {utils: Cu, interfaces: Ci} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
|
||||
const PBKDF2_ROUNDS = 1000;
|
||||
const STRETCHED_PW_LENGTH_BYTES = 32;
|
||||
const HKDF_SALT = CommonUtils.hexToBytes("00");
|
||||
const HKDF_LENGTH = 32;
|
||||
const HMAC_ALGORITHM = Ci.nsICryptoHMAC.SHA256;
|
||||
const HMAC_LENGTH = 32;
|
||||
|
||||
// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
|
||||
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
|
||||
// default.
|
||||
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
|
||||
try {
|
||||
this.LOG_LEVEL =
|
||||
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
|
||||
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
|
||||
} catch (e) {
|
||||
this.LOG_LEVEL = Log.Level.Error;
|
||||
}
|
||||
|
||||
let log = Log.repository.getLogger("Identity.FxAccounts");
|
||||
log.level = LOG_LEVEL;
|
||||
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
|
||||
this.Credentials = Object.freeze({
|
||||
/**
|
||||
* Make constants accessible to tests
|
||||
*/
|
||||
constants: {
|
||||
PROTOCOL_VERSION: PROTOCOL_VERSION,
|
||||
PBKDF2_ROUNDS: PBKDF2_ROUNDS,
|
||||
STRETCHED_PW_LENGTH_BYTES: STRETCHED_PW_LENGTH_BYTES,
|
||||
HKDF_SALT: HKDF_SALT,
|
||||
HKDF_LENGTH: HKDF_LENGTH,
|
||||
HMAC_ALGORITHM: HMAC_ALGORITHM,
|
||||
HMAC_LENGTH: HMAC_LENGTH,
|
||||
},
|
||||
|
||||
/**
|
||||
* KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*
|
||||
* keyWord derivation for use as a salt.
|
||||
*
|
||||
*
|
||||
* @param {String} context String for use in generating salt
|
||||
*
|
||||
* @return {bitArray} the salt
|
||||
*
|
||||
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
|
||||
* Firefox Accounts API.
|
||||
*/
|
||||
keyWord: function(context) {
|
||||
return CommonUtils.stringToBytes(PROTOCOL_VERSION + context);
|
||||
},
|
||||
|
||||
/**
|
||||
* KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*
|
||||
* keyWord extended with a name and an email.
|
||||
*
|
||||
* @param {String} name The name of the salt
|
||||
* @param {String} email The email of the user.
|
||||
*
|
||||
* @return {bitArray} the salt combination with the namespace
|
||||
*
|
||||
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
|
||||
* Firefox Accounts API.
|
||||
*/
|
||||
keyWordExtended: function(name, email) {
|
||||
return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ':' + email);
|
||||
},
|
||||
|
||||
setup: function(emailInput, passwordInput, options={}) {
|
||||
let deferred = Promise.defer();
|
||||
log.debug("setup credentials for " + emailInput);
|
||||
|
||||
let hkdfSalt = options.hkdfSalt || HKDF_SALT;
|
||||
let hkdfLength = options.hkdfLength || HKDF_LENGTH;
|
||||
let hmacLength = options.hmacLength || HMAC_LENGTH;
|
||||
let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
|
||||
let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
|
||||
let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;
|
||||
|
||||
let result = {
|
||||
emailUTF8: emailInput,
|
||||
passwordUTF8: passwordInput,
|
||||
};
|
||||
|
||||
let password = CommonUtils.encodeUTF8(passwordInput);
|
||||
let salt = this.keyWordExtended("quickStretch", emailInput);
|
||||
|
||||
let runnable = () => {
|
||||
let start = Date.now();
|
||||
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
|
||||
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
|
||||
|
||||
result.quickStretchedPW = quickStretchedPW;
|
||||
|
||||
result.authPW =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
|
||||
|
||||
result.unwrapBKey =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
|
||||
|
||||
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
|
||||
deferred.resolve(result);
|
||||
}
|
||||
|
||||
Services.tm.currentThread.dispatch(runnable,
|
||||
Ci.nsIThread.DISPATCH_NORMAL);
|
||||
log.debug("Dispatched thread for credentials setup crypto work");
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ Cu.import("resource://services-common/utils.js");
|
|||
Cu.import("resource://services-common/hawk.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
|
||||
// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
|
||||
let _host = "https://api-accounts.dev.lcip.org/v1";
|
||||
|
@ -21,34 +22,6 @@ try {
|
|||
} catch(keepDefault) {}
|
||||
|
||||
const HOST = _host;
|
||||
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
function KW(context) {
|
||||
// This is used as a salt. It's specified by the protocol. Note that the
|
||||
// value of PROTOCOL_VERSION does not refer in any wy to the version of the
|
||||
// Firefox Accounts API. For this reason, it is not exposed as a pref.
|
||||
//
|
||||
// See:
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#creating-the-account
|
||||
return PROTOCOL_VERSION + context;
|
||||
}
|
||||
|
||||
function stringToHex(str) {
|
||||
let encoder = new TextEncoder("utf-8");
|
||||
let bytes = encoder.encode(str);
|
||||
return bytesToHex(bytes);
|
||||
}
|
||||
|
||||
// XXX Sadly, CommonUtils.bytesAsHex doesn't handle typed arrays.
|
||||
function bytesToHex(bytes) {
|
||||
let hex = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
hex.push((bytes[i] >>> 4).toString(16));
|
||||
hex.push((bytes[i] & 0xF).toString(16));
|
||||
}
|
||||
return hex.join("");
|
||||
}
|
||||
|
||||
this.FxAccountsClient = function(host = HOST) {
|
||||
this.host = host;
|
||||
|
||||
|
@ -92,23 +65,18 @@ this.FxAccountsClient.prototype = {
|
|||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* uid: the user's unique ID (hex)
|
||||
* sessionToken: a session token (hex)
|
||||
* keyFetchToken: a key fetch token (hex)
|
||||
* }
|
||||
*/
|
||||
signUp: function (email, password) {
|
||||
let uid;
|
||||
let hexEmail = stringToHex(email);
|
||||
let uidPromise = this._request("/raw_password/account/create", "POST", null,
|
||||
{email: hexEmail, password: password});
|
||||
|
||||
return uidPromise.then((result) => {
|
||||
uid = result.uid;
|
||||
return this.signIn(email, password)
|
||||
.then(function(result) {
|
||||
result.uid = uid;
|
||||
return result;
|
||||
});
|
||||
signUp: function(email, password) {
|
||||
return Credentials.setup(email, password).then((creds) => {
|
||||
let data = {
|
||||
email: creds.emailUTF8,
|
||||
authPW: CommonUtils.bytesAsHex(creds.authPW),
|
||||
};
|
||||
return this._request("/account/create", "POST", null, data);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -122,15 +90,20 @@ this.FxAccountsClient.prototype = {
|
|||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* uid: the user's unique ID (hex)
|
||||
* sessionToken: a session token (hex)
|
||||
* keyFetchToken: a key fetch token (hex)
|
||||
* verified: flag indicating verification status of the email
|
||||
* }
|
||||
*/
|
||||
signIn: function signIn(email, password) {
|
||||
let hexEmail = stringToHex(email);
|
||||
return this._request("/raw_password/session/create", "POST", null,
|
||||
{email: hexEmail, password: password});
|
||||
return Credentials.setup(email, password).then((creds) => {
|
||||
let data = {
|
||||
email: creds.emailUTF8,
|
||||
authPW: CommonUtils.bytesAsHex(creds.authPW),
|
||||
};
|
||||
return this._request("/account/login", "POST", null, data);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -177,15 +150,16 @@ this.FxAccountsClient.prototype = {
|
|||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* kA: an encryption key for recevorable data
|
||||
* wrapKB: an encryption key that requires knowledge of the user's password
|
||||
* kA: an encryption key for recevorable data (bytes)
|
||||
* wrapKB: an encryption key that requires knowledge of the
|
||||
* user's password (bytes)
|
||||
* }
|
||||
*/
|
||||
accountKeys: function (keyFetchTokenHex) {
|
||||
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
|
||||
let keyRequestKey = creds.extra.slice(0, 32);
|
||||
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
|
||||
KW("account/keys"), 3 * 32);
|
||||
Credentials.keyWord("account/keys"), 3 * 32);
|
||||
let respHMACKey = morecreds.slice(0, 32);
|
||||
let respXORKey = morecreds.slice(32, 96);
|
||||
|
||||
|
@ -251,22 +225,25 @@ this.FxAccountsClient.prototype = {
|
|||
* if it doesn't. The promise is rejected on other errors.
|
||||
*/
|
||||
accountExists: function (email) {
|
||||
let hexEmail = stringToHex(email);
|
||||
return this._request("/auth/start", "POST", null, { email: hexEmail })
|
||||
.then(
|
||||
// the account exists
|
||||
(result) => true,
|
||||
(err) => {
|
||||
log.error("accountExists: error: " + JSON.stringify(err));
|
||||
// the account doesn't exist
|
||||
if (err.errno === 102) {
|
||||
log.debug("returning false for errno 102");
|
||||
return this.signIn(email, "").then(
|
||||
(cantHappen) => {
|
||||
throw new Error("How did I sign in with an empty password?");
|
||||
},
|
||||
(expectedError) => {
|
||||
switch (expectedError.errno) {
|
||||
case ERRNO_ACCOUNT_DOES_NOT_EXIST:
|
||||
return false;
|
||||
}
|
||||
// propogate other request errors
|
||||
throw err;
|
||||
break;
|
||||
case ERRNO_INCORRECT_PASSWORD:
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
// not so expected, any more ...
|
||||
throw expectedError;
|
||||
break;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -291,7 +268,7 @@ this.FxAccountsClient.prototype = {
|
|||
*/
|
||||
_deriveHawkCredentials: function (tokenHex, context, size) {
|
||||
let token = CommonUtils.hexToBytes(tokenHex);
|
||||
let out = CryptoUtils.hkdf(token, undefined, KW(context), size || 3 * 32);
|
||||
let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size || 3 * 32);
|
||||
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
|
@ -333,11 +310,13 @@ this.FxAccountsClient.prototype = {
|
|||
let response = JSON.parse(responseText);
|
||||
deferred.resolve(response);
|
||||
} catch (err) {
|
||||
log.error("json parse error on response: " + responseText);
|
||||
deferred.reject({error: err});
|
||||
}
|
||||
},
|
||||
|
||||
(error) => {
|
||||
log.error("request error: " + JSON.stringify(error));
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -49,7 +49,7 @@ this.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
|
|||
// Server errno.
|
||||
// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
|
||||
this.ERRNO_ACCOUNT_ALREADY_EXISTS = 101;
|
||||
this.ERRNO_ACCOUNT_DOES_NOT_EXISTS = 102;
|
||||
this.ERRNO_ACCOUNT_DOES_NOT_EXIST = 102;
|
||||
this.ERRNO_INCORRECT_PASSWORD = 103;
|
||||
this.ERRNO_UNVERIFIED_ACCOUNT = 104;
|
||||
this.ERRNO_INVALID_VERIFICATION_CODE = 105;
|
||||
|
@ -68,7 +68,7 @@ this.ERRNO_UNKNOWN_ERROR = 999;
|
|||
|
||||
// Errors.
|
||||
this.ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS";
|
||||
this.ERROR_ACCOUNT_DOES_NOT_EXISTS = "ACCOUNT_DOES_NOT_EXISTS";
|
||||
this.ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST";
|
||||
this.ERROR_ALREADY_SIGNED_IN_USER = "ALREADY_SIGNED_IN_USER";
|
||||
this.ERROR_INVALID_ACCOUNTID = "INVALID_ACCOUNTID";
|
||||
this.ERROR_INVALID_AUDIENCE = "INVALID_AUDIENCE";
|
||||
|
@ -96,7 +96,7 @@ this.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
|
|||
// Error matching.
|
||||
this.SERVER_ERRNO_TO_ERROR = {};
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_ALREADY_EXISTS] = ERROR_ACCOUNT_ALREADY_EXISTS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXISTS] = ERROR_ACCOUNT_DOES_NOT_EXISTS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXIST] = ERROR_ACCOUNT_DOES_NOT_EXIST;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_PASSWORD] = ERROR_INVALID_PASSWORD;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
|
||||
|
|
|
@ -9,6 +9,7 @@ PARALLEL_DIRS += ['interfaces']
|
|||
TEST_DIRS += ['tests']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'Credentials.jsm',
|
||||
'FxAccounts.jsm',
|
||||
'FxAccountsClient.jsm',
|
||||
'FxAccountsCommon.js',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
|
||||
|
||||
|
@ -101,131 +102,396 @@ add_task(function test_500_error() {
|
|||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_api_endpoints() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: "NotARealToken"});
|
||||
let creationMessage = JSON.stringify({uid: "NotARealUid"});
|
||||
let signoutMessage = JSON.stringify({});
|
||||
let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
|
||||
let emailStatus = JSON.stringify({verified: true});
|
||||
add_task(function test_signUp() {
|
||||
let creationMessage = JSON.stringify({
|
||||
uid: "uid",
|
||||
sessionToken: "sessionToken",
|
||||
keyFetchToken: "keyFetchToken"
|
||||
});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 101, error: "account exists"});
|
||||
let created = false;
|
||||
|
||||
let authStarts = 0;
|
||||
let server = httpd_setup({
|
||||
"/account/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
function writeResp(response, msg) {
|
||||
response.bodyOutputStream.write(msg, msg.length);
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
|
||||
do_check_eq(jsonBody.email, "andré@example.org");
|
||||
|
||||
if (!created) {
|
||||
do_check_eq(jsonBody.authPW, "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375");
|
||||
created = true;
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
return response.bodyOutputStream.write(creationMessage, creationMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to create same account a second time
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signUp('andré@example.org', 'pässwörd');
|
||||
do_check_eq("uid", result.uid);
|
||||
do_check_eq("sessionToken", result.sessionToken);
|
||||
do_check_eq("keyFetchToken", result.keyFetchToken);
|
||||
|
||||
// Try to create account again. Triggers error path.
|
||||
try {
|
||||
result = yield client.signUp('andré@example.org', 'pässwörd');
|
||||
} catch(expectedError) {
|
||||
do_check_eq(101, expectedError.errno);
|
||||
}
|
||||
|
||||
let server = httpd_setup(
|
||||
{
|
||||
"/raw_password/account/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
|
||||
do_check_eq(jsonBody.password, "biggersecret");
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(creationMessage, creationMessage.length);
|
||||
},
|
||||
"/raw_password/session/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
if (jsonBody.password === "bigsecret") {
|
||||
do_check_eq(jsonBody.email, "6dc3a9406578616d706c652e636f6d");
|
||||
} else if (jsonBody.password === "biggersecret") {
|
||||
do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
|
||||
}
|
||||
add_task(function test_signIn() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let server = httpd_setup({
|
||||
"/account/login": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
if (jsonBody.email == "mé@example.com") {
|
||||
do_check_eq(jsonBody.authPW, "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6");
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
|
||||
},
|
||||
"/recovery_email/status": function(request, response) {
|
||||
return response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to sign in to nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.signIn("yøü@bad.example.org", "nofear");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_signOut() {
|
||||
let signoutMessage = JSON.stringify({});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let signedOut = false;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/session/destroy": function(request, response) {
|
||||
if (!signedOut) {
|
||||
signedOut = true;
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(emailStatus, emailStatus.length);
|
||||
},
|
||||
"/session/destroy": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
return response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to sign out of nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signOut("FakeSession");
|
||||
do_check_eq(typeof result, "object");
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.signOut("FakeSession");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_recoveryEmailStatus() {
|
||||
let emailStatus = JSON.stringify({verified: true});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/recovery_email/status": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
if (tries === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
|
||||
},
|
||||
"/certificate/sign": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
return response.bodyOutputStream.write(emailStatus, emailStatus.length);
|
||||
}
|
||||
|
||||
// Second call gets an error trying to query a nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(result.verified, true);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.recoveryEmailStatus("some bogus session");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_resendVerificationEmail() {
|
||||
let emptyMessage = "{}";
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/recovery_email/resend_code": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
if (tries === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
return response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
}
|
||||
|
||||
// Second call gets an error trying to query a nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.resendVerificationEmail(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(JSON.stringify(result), emptyMessage);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.resendVerificationEmail("some bogus session");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_accountKeys() {
|
||||
// Vectors: https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
|
||||
|
||||
let keyFetch = h("8081828384858687 88898a8b8c8d8e8f"+
|
||||
"9091929394959697 98999a9b9c9d9e9f");
|
||||
|
||||
let response = h("ee5c58845c7c9412 b11bbd20920c2fdd"+
|
||||
"d83c33c9cd2c2de2 d66b222613364636"+
|
||||
"c2c0f8cfbb7c6304 72c0bd88451342c6"+
|
||||
"c05b14ce342c5ad4 6ad89e84464c993c"+
|
||||
"3927d30230157d08 17a077eef4b20d97"+
|
||||
"6f7a97363faf3f06 4c003ada7d01aa70");
|
||||
|
||||
let kA = h("2021222324252627 28292a2b2c2d2e2f"+
|
||||
"3031323334353637 38393a3b3c3d3e3f");
|
||||
|
||||
let wrapKB = h("4041424344454647 48494a4b4c4d4e4f"+
|
||||
"5051525354555657 58595a5b5c5d5e5f");
|
||||
|
||||
let responseMessage = JSON.stringify({bundle: response});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let emptyMessage = "{}";
|
||||
let attempt = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/account/keys": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
attempt += 1;
|
||||
|
||||
switch(attempt) {
|
||||
case 1:
|
||||
// First time succeeds
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(responseMessage, responseMessage.length);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Second time, return no bundle to trigger client error
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Return gibberish to trigger client MAC error
|
||||
let garbage = response;
|
||||
garbage[0] = 0; // tweak a byte
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(responseMessage, responseMessage.length);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// Trigger error for nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
// First try, all should be good
|
||||
let result = yield client.accountKeys(keyFetch);
|
||||
do_check_eq(CommonUtils.hexToBytes(kA), result.kA);
|
||||
do_check_eq(CommonUtils.hexToBytes(wrapKB), result.wrapKB);
|
||||
|
||||
// Second try, empty bundle should trigger error
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.message, "failed to retrieve keys");
|
||||
}
|
||||
|
||||
// Third try, bad bundle results in MAC error
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.message, "error unbundling encryption keys");
|
||||
}
|
||||
|
||||
// Fourth try, pretend account doesn't exist
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_signCertificate() {
|
||||
let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/certificate/sign": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
if (tries === 0) {
|
||||
tries += 1;
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
|
||||
do_check_eq(jsonBody.duration, 600);
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
|
||||
},
|
||||
"/auth/start": function(request, response) {
|
||||
if (authStarts === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
writeResp(response, JSON.stringify({}));
|
||||
} else if (authStarts === 1) {
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
writeResp(response, JSON.stringify({errno: 102, error: "no such account"}));
|
||||
} else if (authStarts === 2) {
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
writeResp(response, JSON.stringify({errno: 107, error: "boom"}));
|
||||
}
|
||||
authStarts++;
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
|
||||
}
|
||||
|
||||
// Second attempt, trigger error
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = undefined;
|
||||
|
||||
result = yield client.signUp('you@example.com', 'biggersecret');
|
||||
do_check_eq("NotARealUid", result.uid);
|
||||
|
||||
result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
do_check_eq("NotARealToken", result.sessionToken);
|
||||
|
||||
result = yield client.signOut(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(typeof result, "object");
|
||||
|
||||
result = yield client.recoveryEmailStatus('NotARealToken');
|
||||
do_check_eq(result.verified, true);
|
||||
|
||||
result = yield client.signCertificate('NotARealToken', JSON.stringify({foo: "bar"}), 600);
|
||||
let result = yield client.signCertificate(FAKE_SESSION_TOKEN, JSON.stringify({foo: "bar"}), 600);
|
||||
do_check_eq("baz", result.bar);
|
||||
|
||||
result = yield client.accountExists('hey@example.com');
|
||||
do_check_eq(result, true);
|
||||
result = yield client.accountExists('hey2@example.com');
|
||||
do_check_eq(result, false);
|
||||
// Account doesn't exist
|
||||
try {
|
||||
result = yield client.accountExists('hey3@example.com');
|
||||
} catch(e) {
|
||||
do_check_eq(e.errno, 107);
|
||||
result = yield client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_error_response() {
|
||||
let errorMessage = JSON.stringify({error: "Oops", code: 400, errno: 99});
|
||||
add_task(function test_accountExists() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
|
||||
let existsMessage = JSON.stringify({error: "wrong password", code: 400, errno: 103});
|
||||
let doesntExistMessage = JSON.stringify({error: "no such account", code: 400, errno: 102});
|
||||
let emptyMessage = "{}";
|
||||
|
||||
let server = httpd_setup(
|
||||
{
|
||||
"/raw_password/session/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let server = httpd_setup({
|
||||
"/account/login": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
}
|
||||
);
|
||||
switch (jsonBody.email) {
|
||||
// We'll test that these users' accounts exist
|
||||
case "i.exist@example.com":
|
||||
case "i.also.exist@example.com":
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(existsMessage, existsMessage.length);
|
||||
break;
|
||||
|
||||
// This user's account doesn't exist
|
||||
case "i.dont.exist@example.com":
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(doesntExistMessage, doesntExistMessage.length);
|
||||
break;
|
||||
|
||||
// This user throws an unexpected response
|
||||
// This will reject the client signIn promise
|
||||
case "i.break.things@example.com":
|
||||
response.setStatusLine(request.httpVersion, 500, "Alas");
|
||||
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unexpected login from " + jsonBody.email);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result;
|
||||
|
||||
try {
|
||||
let result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
} catch(result) {
|
||||
do_check_eq("Oops", result.error);
|
||||
do_check_eq(400, result.code);
|
||||
do_check_eq(99, result.errno);
|
||||
result = yield client.accountExists("i.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.code, 400);
|
||||
do_check_eq(expectedError.errno, 103);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.also.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.errno, 103);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.dont.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.errno, 102);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.break.things@example.com");
|
||||
} catch(unexpectedError) {
|
||||
do_check_eq(unexpectedError.code, 500);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
let {hexToBytes: h2b,
|
||||
hexAsString: h2s,
|
||||
stringAsHex: s2h,
|
||||
bytesAsHex: b2h} = CommonUtils;
|
||||
|
||||
// Test vectors for the "onepw" protocol:
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
|
||||
let vectors = {
|
||||
"client stretch-KDF": {
|
||||
email:
|
||||
h("616e6472c3a94065 78616d706c652e6f 7267"),
|
||||
password:
|
||||
h("70c3a4737377c3b6 7264"),
|
||||
quickStretchedPW:
|
||||
h("e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d"),
|
||||
authPW:
|
||||
h("247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375"),
|
||||
authSalt:
|
||||
h("00f0000000000000 0000000000000000 0000000000000000 0000000000000000"),
|
||||
},
|
||||
};
|
||||
|
||||
// A simple test suite with no utf8 encoding madness.
|
||||
add_task(function test_onepw_setup_credentials() {
|
||||
let email = "francine@example.org";
|
||||
let password = CommonUtils.encodeUTF8("i like pie");
|
||||
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let hkdf = CryptoUtils.hkdf;
|
||||
|
||||
// quickStretch the email
|
||||
let saltyEmail = Credentials.keyWordExtended("quickStretch", email);
|
||||
|
||||
do_check_eq(b2h(saltyEmail), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a6672616e63696e65406578616d706c652e6f7267");
|
||||
|
||||
let pbkdf2Rounds = 1000;
|
||||
let pbkdf2Len = 32;
|
||||
|
||||
let quickStretchedPW = pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len, Ci.nsICryptoHMAC.SHA256, 32);
|
||||
let quickStretchedActual = "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4";
|
||||
do_check_eq(b2h(quickStretchedPW), quickStretchedActual);
|
||||
|
||||
// obtain hkdf info
|
||||
let authKeyInfo = Credentials.keyWord('authPW');
|
||||
do_check_eq(b2h(authKeyInfo), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f617574685057");
|
||||
|
||||
// derive auth password
|
||||
let hkdfSalt = h2b("00");
|
||||
let hkdfLen = 32;
|
||||
let authPW = hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen);
|
||||
|
||||
do_check_eq(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342");
|
||||
|
||||
// derive unwrap key
|
||||
let unwrapKeyInfo = Credentials.keyWord('unwrapBkey');
|
||||
let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
|
||||
|
||||
do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_client_stretch_kdf() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let hkdf = CryptoUtils.hkdf;
|
||||
let expected = vectors["client stretch-KDF"];
|
||||
|
||||
let emailUTF8 = h2s(expected.email);
|
||||
let passwordUTF8 = h2s(expected.password);
|
||||
|
||||
// Intermediate value from sjcl implementation in fxa-js-client
|
||||
// The key thing is the c3a9 sequence in "andré"
|
||||
let salt = Credentials.keyWordExtended("quickStretch", emailUTF8);
|
||||
do_check_eq(b2h(salt), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a616e6472c3a9406578616d706c652e6f7267");
|
||||
|
||||
let options = {
|
||||
stretchedPassLength: 32,
|
||||
pbkdf2Rounds: 1000,
|
||||
hmacAlgorithm: Ci.nsICryptoHMAC.SHA256,
|
||||
hmacLength: 32,
|
||||
hkdfSalt: h2b("00"),
|
||||
hkdfLength: 32,
|
||||
};
|
||||
|
||||
let results = yield Credentials.setup(emailUTF8, passwordUTF8, options);
|
||||
|
||||
do_check_eq(emailUTF8, results.emailUTF8,
|
||||
"emailUTF8 is wrong");
|
||||
|
||||
do_check_eq(passwordUTF8, results.passwordUTF8,
|
||||
"passwordUTF8 is wrong");
|
||||
|
||||
do_check_eq(expected.quickStretchedPW, b2h(results.quickStretchedPW),
|
||||
"quickStretchedPW is wrong");
|
||||
|
||||
do_check_eq(expected.authPW, b2h(results.authPW),
|
||||
"authPW is wrong");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// End of tests
|
||||
// Utility functions follow
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
|
@ -4,6 +4,7 @@ tail =
|
|||
|
||||
[test_accounts.js]
|
||||
[test_client.js]
|
||||
[test_credentials.js]
|
||||
[test_manager.js]
|
||||
run-if = appname == 'b2g'
|
||||
reason = FxAccountsManager is only available for B2G for now
|
||||
|
|
Загрузка…
Ссылка в новой задаче