Bug 1518300 - Refactor CryptoUtils and add JWK/JWE methods to jwcrypto. r=rfkelly,tjr

Differential Revision: https://phabricator.services.mozilla.com/D15868

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Edouard Oger 2019-01-18 20:58:42 +00:00
parent 600eb759a4
commit f35506cf5d
25 changed files with 601 additions and 1024 deletions

View File

@ -79,7 +79,7 @@ HAWKAuthenticatedRESTRequest.prototype = {
payload: data && JSON.stringify(data) || "",
contentType,
};
let header = CryptoUtils.computeHAWK(this.uri, method, options);
let header = await CryptoUtils.computeHAWK(this.uri, method, options);
this.setHeader("Authorization", header.field);
}
@ -113,22 +113,17 @@ HAWKAuthenticatedRESTRequest.prototype = {
* @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)
* extra: size - 64 extra bytes (if size > 64)
* }
*/
function deriveHawkCredentials(tokenHex,
context,
size = 96,
hexKey = false) {
async function deriveHawkCredentials(tokenHex, context, size = 96) {
let token = CommonUtils.hexToBytes(tokenHex);
let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size);
let out = await CryptoUtils.hkdfLegacy(token, undefined, Credentials.keyWord(context), size);
let result = {
algorithm: "sha256",
key: hexKey ? CommonUtils.bytesAsHex(out.slice(32, 64)) : out.slice(32, 64),
key: out.slice(32, 64),
id: CommonUtils.bytesAsHex(out.slice(0, 32)),
};
if (size > 64) {

View File

@ -658,7 +658,7 @@ TokenAuthenticatedRESTRequest.prototype = {
__proto__: RESTRequest.prototype,
async dispatch(method, data) {
let sig = CryptoUtils.computeHTTPMACSHA1(
let sig = await CryptoUtils.computeHTTPMACSHA1(
this.authToken.id, this.authToken.key, method, this.uri, this.extra
);

View File

@ -469,16 +469,6 @@ add_task(async function test_401_then_500() {
await promiseStopServer(server);
});
add_task(async function throw_if_not_json_body() {
let client = new HawkClient("https://example.com");
try {
await client.request("/bogus", "GET", {}, "I am not json");
do_throw("Expected an error");
} catch (err) {
Assert.ok(!!err.message);
}
});
// End of tests.
// Utility functions follow

View File

@ -197,11 +197,8 @@ add_task(async function test_hawk_language_pref_changed() {
await promiseStopServer(server);
});
add_task(function test_deriveHawkCredentials() {
let credentials = deriveHawkCredentials(
SESSION_KEYS.sessionToken, "sessionToken");
Assert.equal(credentials.algorithm, "sha256");
add_task(async function test_deriveHawkCredentials() {
let credentials = await deriveHawkCredentials(SESSION_KEYS.sessionToken, "sessionToken");
Assert.equal(credentials.id, SESSION_KEYS.tokenID);
Assert.equal(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey);
});

View File

@ -22,7 +22,7 @@ add_task(async function test_authenticated_request() {
let key = "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=";
let method = "GET";
let nonce = btoa(CryptoUtils.generateRandomBytes(16));
let nonce = btoa(CryptoUtils.generateRandomBytesLegacy(16));
let ts = Math.floor(Date.now() / 1000);
let extra = {ts, nonce};
@ -37,7 +37,7 @@ add_task(async function test_authenticated_request() {
},
});
let uri = CommonUtils.makeURI(server.baseURI + "/foo");
let sig = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, extra);
let sig = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, extra);
auth = sig.getHeader();
let req = new TokenAuthenticatedRESTRequest(uri, {id, key}, extra);

View File

@ -197,6 +197,28 @@ var CommonUtils = {
return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0));
},
// A lot of Util methods work with byte strings instead of ArrayBuffers.
// A patch should address this problem, but in the meantime let's provide
// helpers method to convert byte strings to Uint8Array.
byteStringToArrayBuffer(byteString) {
if (byteString === undefined) {
return new Uint8Array();
}
const bytes = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; ++i) {
bytes[i] = byteString.charCodeAt(i) & 0xff;
}
return bytes;
},
arrayBufferToByteString(buffer) {
return CommonUtils.byteArrayToString([...buffer]);
},
bufferToHex(buffer) {
return Array.prototype.map.call(buffer, (x) => ("00" + x.toString(16)).slice(-2)).join("");
},
bytesAsHex: function bytesAsHex(bytes) {
let s = "";
for (let i = 0, len = bytes.length; i < len; i++) {
@ -225,6 +247,11 @@ var CommonUtils = {
return String.fromCharCode.apply(String, bytes);
},
hexToArrayBuffer(str) {
const octString = CommonUtils.hexToBytes(str);
return CommonUtils.byteStringToArrayBuffer(octString);
},
hexAsString: function hexAsString(hex) {
return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
},

View File

@ -1,5 +1,3 @@
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */
@ -15,20 +13,20 @@ XPCOMUtils.defineLazyServiceGetter(this,
"IdentityCryptoService",
"@mozilla.org/identity/crypto-service;1",
"nsIIdentityCryptoService");
XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
var EXPORTED_SYMBOLS = ["jwcrypto"];
const EXPORTED_SYMBOLS = ["jwcrypto"];
const PREF_LOG_LEVEL = "services.crypto.jwcrypto.log.level";
XPCOMUtils.defineLazyGetter(this, "log", function() {
let log = Log.repository.getLogger("Services.Crypto.jwcrypto");
const log = Log.repository.getLogger("Services.Crypto.jwcrypto");
// Default log level is "Error", but consumers can change this with the pref
// "services.crypto.jwcrypto.log.level".
log.level = Log.Level.Error;
let appender = new Log.DumpAppender();
const appender = new Log.DumpAppender();
log.addAppender(appender);
try {
let level =
const 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;
@ -39,94 +37,164 @@ XPCOMUtils.defineLazyGetter(this, "log", function() {
return log;
});
const ALGORITHMS = { RS256: "RS256", DS160: "DS160" };
const DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
const ASSERTION_DEFAULT_DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
const ECDH_PARAMS = {
name: "ECDH",
namedCurve: "P-256",
};
const AES_PARAMS = {
name: "AES-GCM",
length: 256,
};
const AES_TAG_LEN = 128;
const AES_GCM_IV_SIZE = 12;
const UTF8_ENCODER = new TextEncoder();
const UTF8_DECODER = new TextDecoder();
function generateKeyPair(aAlgorithmName, aCallback) {
log.debug("Generate key pair; alg = " + aAlgorithmName);
IdentityCryptoService.generateKeyPair(aAlgorithmName, function(rv, aKeyPair) {
if (!Components.isSuccessCode(rv)) {
return aCallback("key generation failed");
}
var publicKey;
switch (aKeyPair.keyType) {
case ALGORITHMS.RS256:
publicKey = {
algorithm: "RS",
exponent: aKeyPair.hexRSAPublicKeyExponent,
modulus: aKeyPair.hexRSAPublicKeyModulus,
};
break;
case ALGORITHMS.DS160:
publicKey = {
algorithm: "DS",
y: aKeyPair.hexDSAPublicValue,
p: aKeyPair.hexDSAPrime,
q: aKeyPair.hexDSASubPrime,
g: aKeyPair.hexDSAGenerator,
};
break;
default:
return aCallback("unknown key type");
}
let keyWrapper = {
serializedPublicKey: JSON.stringify(publicKey),
_kp: aKeyPair,
};
return aCallback(null, keyWrapper);
});
}
function sign(aPayload, aKeypair, aCallback) {
aKeypair._kp.sign(aPayload, function(rv, signature) {
if (!Components.isSuccessCode(rv)) {
log.error("signer.sign failed");
return aCallback("Sign failed");
}
log.debug("signer.sign: success");
return aCallback(null, signature);
});
}
function jwcryptoClass() {
}
jwcryptoClass.prototype = {
/*
* Determine the expiration of the assertion. Returns expiry date
* in milliseconds as integer.
class JWCrypto {
/**
* Encrypts the given data into a JWE using AES-256-GCM content encryption.
*
* @param localtimeOffsetMsec (optional)
* The number of milliseconds that must be added to the local clock
* for it to agree with the server. For example, if the local clock
* if two minutes fast, localtimeOffsetMsec would be -120000
* This function implements a very small subset of the JWE encryption standard
* from https://tools.ietf.org/html/rfc7516. The only supported content encryption
* algorithm is enc="A256GCM" [1] and the only supported key encryption algorithm
* is alg="ECDH-ES" [2].
* The IV is generated randomly: if you are using long-lived keys you might be
* exposing yourself to a birthday attack. Please consult your nearest cryptographer.
*
* @param now (options)
* Current date in milliseconds. Useful for mocking clock
* skew in testing.
* @param {Object} key Peer Public JWK.
* @param {ArrayBuffer} data
*
* [1] https://tools.ietf.org/html/rfc7518#section-5.3
* [2] https://tools.ietf.org/html/rfc7518#section-4.6
*
* @returns {Promise<String>}
*/
getExpiration(duration = DURATION_MS, localtimeOffsetMsec = 0, now = Date.now()) {
return now + localtimeOffsetMsec + duration;
},
async generateJWE(key, data) {
// Generate an ephemeral key to use just for this encryption.
const epk = await crypto.subtle.generateKey(ECDH_PARAMS, true, ["deriveKey"]);
const peerPublicKey = await crypto.subtle.importKey("jwk", key, ECDH_PARAMS, false, ["deriveKey"]);
return this._generateJWE(epk, peerPublicKey, data);
}
isCertValid(aCert, aCallback) {
// XXX check expiration, bug 769850
aCallback(true);
},
async _generateJWE(epk, peerPublicKey, data) {
let iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_SIZE));
const ownPublicJWK = await crypto.subtle.exportKey("jwk", epk.publicKey);
delete ownPublicJWK.key_ops;
// Do ECDH agreement to get the content encryption key.
const contentKey = await deriveECDHSharedAESKey(epk.privateKey, peerPublicKey, ["encrypt"]);
let header = {alg: "ECDH-ES", enc: "A256GCM", epk: ownPublicJWK};
// Yes, additionalData is the byte representation of the base64 representation of the stringified header.
const additionalData = UTF8_ENCODER.encode(ChromeUtils.base64URLEncode(UTF8_ENCODER.encode(JSON.stringify(header)), {pad: false}));
const encrypted = await crypto.subtle.encrypt({
name: "AES-GCM",
iv,
additionalData,
tagLength: AES_TAG_LEN,
},
contentKey,
data,
);
const tagIdx = encrypted.byteLength - ((AES_TAG_LEN + 7) >> 3);
let ciphertext = encrypted.slice(0, tagIdx);
let tag = encrypted.slice(tagIdx);
// JWE serialization.
header = UTF8_ENCODER.encode(JSON.stringify(header));
header = ChromeUtils.base64URLEncode(header, {pad: false});
tag = ChromeUtils.base64URLEncode(tag, {pad: false});
ciphertext = ChromeUtils.base64URLEncode(ciphertext, {pad: false});
iv = ChromeUtils.base64URLEncode(iv, {pad: false});
return `${header}..${iv}.${ciphertext}.${tag}`; // No CEK
}
/**
* Decrypts the given JWE using AES-256-GCM content encryption into a byte array.
* This function does the opposite of `JWCrypto.generateJWE`.
* The only supported content encryption algorithm is enc="A256GCM" [1]
* and the only supported key encryption algorithm is alg="ECDH-ES" [2].
*
* @param {"ECDH-ES"} algorithm
* @param {CryptoKey} key Local private key
*
* [1] https://tools.ietf.org/html/rfc7518#section-5.3
* [2] https://tools.ietf.org/html/rfc7518#section-4.6
*
* @returns {Promise<Uint8Array>}
*/
async decryptJWE(jwe, key) {
let [header, cek, iv, ciphertext, authTag] = jwe.split(".");
const additionalData = UTF8_ENCODER.encode(header);
header = JSON.parse(UTF8_DECODER.decode(ChromeUtils.base64URLDecode(header, {padding: "reject"})));
if (cek.length > 0 || header.enc !== "A256GCM" || header.alg !== "ECDH-ES") {
throw new Error("Unknown algorithm.");
}
if ("apu" in header || "apv" in header) {
throw new Error("apu and apv header values are not supported.");
}
const peerPublicKey = await crypto.subtle.importKey("jwk", header.epk, ECDH_PARAMS, false, ["deriveKey"]);
// Do ECDH agreement to get the content encryption key.
const contentKey = await deriveECDHSharedAESKey(key, peerPublicKey, ["decrypt"]);
iv = ChromeUtils.base64URLDecode(iv, {padding: "reject"});
ciphertext = new Uint8Array(ChromeUtils.base64URLDecode(ciphertext, {padding: "reject"}));
authTag = new Uint8Array(ChromeUtils.base64URLDecode(authTag, {padding: "reject"}));
const bundle = new Uint8Array([...ciphertext, ...authTag]);
const decrypted = await crypto.subtle.decrypt({
name: "AES-GCM",
iv,
tagLength: AES_TAG_LEN,
additionalData,
},
contentKey,
bundle
);
return new Uint8Array(decrypted);
}
generateKeyPair(aAlgorithmName, aCallback) {
log.debug("generating");
generateKeyPair(aAlgorithmName, aCallback);
},
log.debug("Generate key pair; alg = " + aAlgorithmName);
/*
IdentityCryptoService.generateKeyPair(aAlgorithmName, (rv, aKeyPair) => {
if (!Components.isSuccessCode(rv)) {
return aCallback("key generation failed");
}
let publicKey;
switch (aKeyPair.keyType) {
case "RS256":
publicKey = {
algorithm: "RS",
exponent: aKeyPair.hexRSAPublicKeyExponent,
modulus: aKeyPair.hexRSAPublicKeyModulus,
};
break;
case "DS160":
publicKey = {
algorithm: "DS",
y: aKeyPair.hexDSAPublicValue,
p: aKeyPair.hexDSAPrime,
q: aKeyPair.hexDSASubPrime,
g: aKeyPair.hexDSAGenerator,
};
break;
default:
return aCallback("unknown key type");
}
const keyWrapper = {
serializedPublicKey: JSON.stringify(publicKey),
_kp: aKeyPair,
};
return aCallback(null, keyWrapper);
});
}
/**
* Generate an assertion and return it through the provided callback.
*
* @param aCert
@ -163,29 +231,68 @@ jwcryptoClass.prototype = {
// for now, we hack the algorithm name
// XXX bug 769851
var header = {"alg": "DS128"};
var headerBytes = IdentityCryptoService.base64UrlEncode(
const header = {"alg": "DS128"};
const headerBytes = IdentityCryptoService.base64UrlEncode(
JSON.stringify(header));
var payload = {
exp: this.getExpiration(
aOptions.duration, aOptions.localtimeOffsetMsec, aOptions.now),
function getExpiration(duration = ASSERTION_DEFAULT_DURATION_MS, localtimeOffsetMsec = 0, now = Date.now()) {
return now + localtimeOffsetMsec + duration;
}
const payload = {
exp: getExpiration(aOptions.duration, aOptions.localtimeOffsetMsec, aOptions.now),
aud: aAudience,
};
var payloadBytes = IdentityCryptoService.base64UrlEncode(
const payloadBytes = IdentityCryptoService.base64UrlEncode(
JSON.stringify(payload));
log.debug("payload", { payload, payloadBytes });
sign(headerBytes + "." + payloadBytes, aKeyPair, function(err, signature) {
if (err)
return aCallback(err);
var signedAssertion = headerBytes + "." + payloadBytes + "." + signature;
return aCallback(null, aCert + "~" + signedAssertion);
const message = headerBytes + "." + payloadBytes;
aKeyPair._kp.sign(message, (rv, signature) => {
if (!Components.isSuccessCode(rv)) {
log.error("signer.sign failed");
aCallback("Sign failed");
return;
}
log.debug("signer.sign: success");
const signedAssertion = message + "." + signature;
aCallback(null, aCert + "~" + signedAssertion);
});
},
}
}
};
/**
* Do an ECDH agreement between a public and private key,
* returning the derived encryption key as specced by
* JWA RFC.
* The raw ECDH secret is derived into a key using
* Concat KDF, as defined in Section 5.8.1 of [NIST.800-56A].
* @param {CryptoKey} privateKey
* @param {CryptoKey} publicKey
* @param {String[]} keyUsages See `SubtleCrypto.deriveKey` 5th paramater documentation.
* @returns {Promise<CryptoKey>}
*/
async function deriveECDHSharedAESKey(privateKey, publicKey, keyUsages) {
const params = {...ECDH_PARAMS, ...{public: publicKey}};
const sharedKey = await crypto.subtle.deriveKey(params, privateKey, AES_PARAMS, true, keyUsages);
// This is the NIST Concat KDF specialized to a specific set of parameters,
// which basically turn it into a single application of SHA256.
// The details are from the JWA RFC.
let sharedKeyBytes = await crypto.subtle.exportKey("raw", sharedKey);
sharedKeyBytes = new Uint8Array(sharedKeyBytes);
const info = [
"\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier
"\x00\x00\x00\x00", // empty PartyUInfo
"\x00\x00\x00\x00", // empty PartyVInfo
"\x00\x00\x01\x00", // keylen == 256
].join("");
const pkcs = `\x00\x00\x00\x01${String.fromCharCode.apply(null, sharedKeyBytes)}${info}`;
const pkcsBuf = Uint8Array.from(Array.prototype.map.call(pkcs, (c) => c.charCodeAt(0)));
const derivedKeyBytes = await crypto.subtle.digest({
name: "SHA-256",
}, pkcsBuf);
return crypto.subtle.importKey("raw", derivedKeyBytes, AES_PARAMS, false, keyUsages);
}
var jwcrypto = new jwcryptoClass();
this.jwcrypto.ALGORITHMS = ALGORITHMS;
const jwcrypto = new JWCrypto();

View File

@ -7,9 +7,19 @@ var EXPORTED_SYMBOLS = ["CryptoUtils"];
ChromeUtils.import("resource://services-common/observers.js");
ChromeUtils.import("resource://services-common/utils.js");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
XPCOMUtils.defineLazyGetter(this, "textEncoder",
function() { return new TextEncoder(); }
);
/**
* A number of `Legacy` suffixed functions are exposed by CryptoUtils.
* They work with octet strings, which were used before Javascript
* got ArrayBuffer and friends.
*/
var CryptoUtils = {
xor: function xor(a, b) {
xor(a, b) {
let bytes = [];
if (a.length != b.length) {
@ -25,19 +35,22 @@ var CryptoUtils = {
/**
* Generate a string of random bytes.
* @returns {String} Octet string
*/
generateRandomBytes: function generateRandomBytes(length) {
let rng = Cc["@mozilla.org/security/random-generator;1"]
.createInstance(Ci.nsIRandomGenerator);
let bytes = rng.generateRandomBytes(length);
return CommonUtils.byteArrayToString(bytes);
generateRandomBytesLegacy(length) {
let bytes = CryptoUtils.generateRandomBytes(length);
return CommonUtils.arrayBufferToByteString(bytes);
},
generateRandomBytes(length) {
return crypto.getRandomValues(new Uint8Array(length));
},
/**
* UTF8-encode a message and hash it with the given hasher. Returns a
* string containing bytes. The hasher is reset if it's an HMAC hasher.
*/
digestUTF8: function digestUTF8(message, hasher) {
digestUTF8(message, hasher) {
let data = this._utf8Converter.convertToByteArray(message, {});
hasher.update(data, data.length);
let result = hasher.finish(false);
@ -48,16 +61,18 @@ var CryptoUtils = {
},
/**
* Treat the given message as a bytes string and hash it with the given
* hasher. Returns a string containing bytes. The hasher is reset if it's
* an HMAC hasher.
* Treat the given message as a bytes string (if necessary) and hash it with
* the given hasher. Returns a string containing bytes.
* The hasher is reset if it's an HMAC hasher.
*/
digestBytes: function digestBytes(message, hasher) {
// No UTF-8 encoding for you, sunshine.
let bytes = new Uint8Array(message.length);
for (let i = 0; i < message.length; ++i) {
bytes[i] = message.charCodeAt(i) & 0xff;
digestBytes(bytes, hasher) {
if (typeof bytes == "string" || bytes instanceof String) {
bytes = CommonUtils.byteStringToArrayBuffer(bytes);
}
return CryptoUtils.digestBytesArray(bytes, hasher);
},
digestBytesArray(bytes, hasher) {
hasher.update(bytes, bytes.length);
let result = hasher.finish(false);
if (hasher instanceof Ci.nsICryptoHMAC) {
@ -77,36 +92,6 @@ var CryptoUtils = {
hasher.update(bytes, bytes.length);
},
/**
* UTF-8 encode a message and perform a SHA-1 over it.
*
* @param message
* (string) Buffer to perform operation on. Should be a JS string.
* It is possible to pass in a string representing an array
* of bytes. But, you probably don't want to UTF-8 encode
* such data and thus should not be using this function.
*
* @return string
* Raw bytes constituting SHA-1 hash. Value is a JS string. Each
* character is the byte value for that offset. Returned string
* always has .length == 20.
*/
UTF8AndSHA1: function UTF8AndSHA1(message) {
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA1);
return CryptoUtils.digestUTF8(message, hasher);
},
sha1: function sha1(message) {
return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message));
},
sha1Base32: function sha1Base32(message) {
return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message));
},
sha256(message) {
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
@ -141,121 +126,84 @@ var CryptoUtils = {
},
/**
* HMAC-based Key Derivation (RFC 5869).
* @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256)
* @param {string} key Key as an octet string.
* @param {string} data Data as an octet string.
*/
hkdf: function hkdf(ikm, xts, info, len) {
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
CryptoUtils.makeHMACKey(xts));
let prk = CryptoUtils.digestBytes(ikm, h);
return CryptoUtils.hkdfExpand(prk, info, len);
async hmacLegacy(alg, key, data) {
if (!key || !key.length) {
key = "\0";
}
data = CommonUtils.byteStringToArrayBuffer(data);
key = CommonUtils.byteStringToArrayBuffer(key);
const result = await CryptoUtils.hmac(alg, key, data);
return CommonUtils.arrayBufferToByteString(result);
},
/**
* HMAC-based Key Derivation Step 2 according to RFC 5869.
* @param {string} ikm IKM as an octet string.
* @param {string} salt Salt as an Hex string.
* @param {string} info Info as a regular string.
* @param {Number} len Desired output length in bytes.
*/
hkdfExpand: function hkdfExpand(prk, info, len) {
const BLOCKSIZE = 256 / 8;
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
CryptoUtils.makeHMACKey(prk));
let T = "";
let Tn = "";
let iterations = Math.ceil(len / BLOCKSIZE);
for (let i = 0; i < iterations; i++) {
Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h);
T += Tn;
}
return T.slice(0, len);
async hkdfLegacy(ikm, xts, info, len) {
ikm = CommonUtils.byteStringToArrayBuffer(ikm);
xts = CommonUtils.byteStringToArrayBuffer(xts);
info = textEncoder.encode(info);
const okm = await CryptoUtils.hkdf(ikm, xts, info, len);
return CommonUtils.arrayBufferToByteString(okm);
},
/**
* PBKDF2 implementation in Javascript.
*
* The arguments to this function correspond to items in
* PKCS #5, v2.0 pp. 9-10
*
* P: the passphrase, an octet string: e.g., "secret phrase"
* S: the salt, an octet string: e.g., "DNXPzPpiwn"
* 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.
* @param {String} alg Hash algorithm (common values are SHA-1 or SHA-256)
* @param {ArrayBuffer} key
* @param {ArrayBuffer} data
* @param {Number} len Desired output length in bytes.
* @returns {Uint8Array}
*/
pbkdf2Generate: function pbkdf2Generate(P, S, c, dkLen,
hmacAlg = Ci.nsICryptoHMAC.SHA1, hmacLen = 20) {
async hmac(alg, key, data) {
const hmacKey = await crypto.subtle.importKey("raw", key, {name: "HMAC", hash: alg}, false, ["sign"]);
const result = await crypto.subtle.sign("HMAC", hmacKey, data);
return new Uint8Array(result);
},
// We don't have a default in the algo itself, as NSS does.
if (!dkLen) {
throw new Error("dkLen should be defined");
}
/**
* @param {ArrayBuffer} ikm
* @param {ArrayBuffer} salt
* @param {ArrayBuffer} info
* @param {Number} len Desired output length in bytes.
* @returns {Uint8Array}
*/
async hkdf(ikm, salt, info, len) {
const key = await crypto.subtle.importKey("raw", ikm, {name: "HKDF"}, false, ["deriveBits"]);
const okm = await crypto.subtle.deriveBits({
name: "HKDF",
hash: "SHA-256",
salt,
info,
}, key, len * 8);
return new Uint8Array(okm);
},
function F(S, c, i, h) {
function XOR(a, b, isA) {
if (a.length != b.length) {
return false;
}
let val = [];
for (let i = 0; i < a.length; i++) {
if (isA) {
val[i] = a[i] ^ b[i];
} else {
val[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
}
}
return val;
}
let ret;
let U = [];
/* Encode i into 4 octets: _INT */
let I = [];
I[0] = String.fromCharCode((i >> 24) & 0xff);
I[1] = String.fromCharCode((i >> 16) & 0xff);
I[2] = String.fromCharCode((i >> 8) & 0xff);
I[3] = String.fromCharCode(i & 0xff);
U[0] = CryptoUtils.digestBytes(S + I.join(""), h);
for (let j = 1; j < c; j++) {
U[j] = CryptoUtils.digestBytes(U[j - 1], h);
}
ret = U[0];
for (let j = 1; j < c; j++) {
ret = CommonUtils.byteArrayToString(XOR(ret, U[j]));
}
return ret;
}
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(hmacAlg,
CryptoUtils.makeHMACKey(P));
let T = [];
for (let i = 0; i < l;) {
T[i] = F(S, c, ++i, h);
}
let ret = "";
for (let i = 0; i < l - 1;) {
ret += T[i++];
}
ret += T[l - 1].substr(0, r);
return ret;
/**
* PBKDF2 password stretching with SHA-256 hmac.
*
* @param {string} passphrase Passphrase as an octet string.
* @param {string} salt Salt as an octet string.
* @param {string} iterations Number of iterations, a positive integer.
* @param {string} len Desired output length in bytes.
*/
async pbkdf2Generate(passphrase, salt, iterations, len) {
passphrase = CommonUtils.byteStringToArrayBuffer(passphrase);
salt = CommonUtils.byteStringToArrayBuffer(salt);
const key = await crypto.subtle.importKey("raw", passphrase, {name: "PBKDF2"}, false, ["deriveBits"]);
const output = await crypto.subtle.deriveBits({
name: "PBKDF2",
hash: "SHA-256",
salt,
iterations,
}, key, len * 8);
return CommonUtils.arrayBufferToByteString(new Uint8Array(output));
},
/**
@ -295,15 +243,14 @@ var CryptoUtils = {
* nonce - (string) Nonce value used.
* ts - (number) Integer seconds since Unix epoch that was used.
*/
computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method,
uri, extra) {
async computeHTTPMACSHA1(identifier, key, method, uri, extra) {
let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000);
let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8;
// We are allowed to use more than the Base64 alphabet if we want.
let nonce = (extra && extra.nonce)
? extra.nonce
: btoa(CryptoUtils.generateRandomBytes(nonce_bytes));
: btoa(CryptoUtils.generateRandomBytesLegacy(nonce_bytes));
let host = uri.asciiHost;
let port;
@ -329,9 +276,7 @@ var CryptoUtils = {
port + "\n" +
ext + "\n";
let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1,
CryptoUtils.makeHMACKey(key));
let mac = CryptoUtils.digestBytes(requestString, hasher);
const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString);
function getHeader() {
return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts,
@ -406,7 +351,6 @@ var CryptoUtils = {
* All three keys are required:
* id - (string) key identifier
* key - (string) raw key bytes
* algorithm - (string) which hash to use: "sha1" or "sha256"
* ext - (string) application-specific data, included in MAC
* localtimeOffsetMsec - (number) local clock offset (vs server)
* payload - (string) payload to include in hash, containing the
@ -432,7 +376,7 @@ var CryptoUtils = {
* for testing as this function will generate a
* cryptographically secure random one if not defined.
* @returns
* (object) Contains results of operation. The object has the
* Promise<Object> Contains results of operation. The object has the
* following keys:
* field - (string) HAWK header, to use in Authorization: header
* artifacts - (object) other generated values:
@ -446,23 +390,11 @@ var CryptoUtils = {
* ext - (string) app-specific data
* MAC - (string) request MAC (base64)
*/
computeHAWK(uri, method, options) {
async computeHAWK(uri, method, options) {
let credentials = options.credentials;
let ts = options.ts || Math.floor(((options.now || Date.now()) +
(options.localtimeOffsetMsec || 0))
/ 1000);
let hash_algo, hmac_algo;
if (credentials.algorithm == "sha1") {
hash_algo = Ci.nsICryptoHash.SHA1;
hmac_algo = Ci.nsICryptoHMAC.SHA1;
} else if (credentials.algorithm == "sha256") {
hash_algo = Ci.nsICryptoHash.SHA256;
hmac_algo = Ci.nsICryptoHMAC.SHA256;
} else {
throw new Error("Unsupported algorithm: " + credentials.algorithm);
}
let port;
if (uri.port != -1) {
port = uri.port;
@ -476,7 +408,7 @@ var CryptoUtils = {
let artifacts = {
ts,
nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)),
nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)),
method: method.toUpperCase(),
resource: uri.pathQueryRef, // This includes both path and search/queryarg.
host: uri.asciiHost.toLowerCase(), // This includes punycoding.
@ -489,18 +421,11 @@ var CryptoUtils = {
if (!artifacts.hash && options.hasOwnProperty("payload")
&& options.payload) {
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hash_algo);
CryptoUtils.updateUTF8("hawk.1.payload\n", hasher);
CryptoUtils.updateUTF8(contentType + "\n", hasher);
CryptoUtils.updateUTF8(options.payload, hasher);
CryptoUtils.updateUTF8("\n", hasher);
let hash = hasher.finish(false);
const buffer = textEncoder.encode(`hawk.1.payload\n${contentType}\n${options.payload}\n`);
const hash = await crypto.subtle.digest("SHA-256", buffer);
// HAWK specifies this .hash to use +/ (not _-) and include the
// trailing "==" padding.
let hash_b64 = btoa(hash);
artifacts.hash = hash_b64;
artifacts.hash = ChromeUtils.base64URLEncode(hash, {pad: true}).replace(/-/g, "+").replace(/_/g, "/");
}
let requestString = ("hawk.1.header\n" +
@ -516,9 +441,8 @@ var CryptoUtils = {
}
requestString += "\n";
let hasher = CryptoUtils.makeHMACHasher(hmac_algo,
CryptoUtils.makeHMACKey(credentials.key));
artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher));
const hash = await CryptoUtils.hmacLegacy("SHA-256", credentials.key, requestString);
artifacts.mac = btoa(hash);
// The output MAC uses "+" and "/", and padded== .
function escape(attribute) {

View File

@ -11,6 +11,8 @@ XPCOMUtils.defineLazyServiceGetter(this,
"@mozilla.org/identity/crypto-service;1",
"nsIIdentityCryptoService");
Cu.importGlobalProperties(["crypto"]);
const RP_ORIGIN = "http://123done.org";
const INTERNAL_ORIGIN = "browserid://";
@ -21,111 +23,99 @@ const HOUR_MS = MINUTE_MS * 60;
// Enable logging from jwcrypto.jsm.
Services.prefs.setCharPref("services.crypto.jwcrypto.log.level", "Debug");
function test_sanity() {
do_test_pending();
jwcrypto.generateKeyPair("DS160", function(err, kp) {
Assert.equal(null, err);
do_test_finished();
run_next_test();
});
}
function test_generate() {
do_test_pending();
jwcrypto.generateKeyPair("DS160", function(err, kp) {
Assert.equal(null, err);
Assert.notEqual(kp, null);
do_test_finished();
run_next_test();
});
}
function test_get_assertion() {
do_test_pending();
jwcrypto.generateKeyPair(
"DS160",
function(err, kp) {
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, (err2, backedAssertion) => {
Assert.equal(null, err2);
Assert.equal(backedAssertion.split("~").length, 2);
Assert.equal(backedAssertion.split(".").length, 3);
do_test_finished();
run_next_test();
function promisify(fn) {
return (...args) => {
return new Promise((res, rej) => {
fn(...args, (err, result) => {
err ? rej(err) : res(result);
});
});
};
}
const generateKeyPair = promisify(jwcrypto.generateKeyPair);
const generateAssertion = promisify(jwcrypto.generateAssertion);
function test_rsa() {
do_test_pending();
function checkRSA(err, kpo) {
Assert.notEqual(kpo, undefined);
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "RS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.exponent, null);
do_check_neq(kpo.modulus, null);
add_task(async function test_jwe_roundtrip_ecdh_es_encryption() {
const data = crypto.getRandomValues(new Uint8Array(123));
const localEpk = await crypto.subtle.generateKey({
name: "ECDH",
namedCurve: "P-256",
}, true, ["deriveKey"]);
const remoteEpk = await crypto.subtle.generateKey({
name: "ECDH",
namedCurve: "P-256",
}, true, ["deriveKey"]);
const jwe = await jwcrypto._generateJWE(localEpk, remoteEpk.publicKey, data);
const decryptedJWE = await jwcrypto.decryptJWE(jwe, remoteEpk.privateKey);
Assert.deepEqual(data, decryptedJWE);
});
// TODO: should sign be async?
let sig = kpo.sign("This is a message to sign");
add_task(async function test_sanity() {
// Shouldn't reject.
await generateKeyPair("DS160");
});
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
do_test_finished();
run_next_test();
}
add_task(async function test_generate() {
let kp = await generateKeyPair("DS160");
Assert.notEqual(kp, null);
});
jwcrypto.generateKeyPair("RS256", checkRSA);
}
add_task(async function test_get_assertion() {
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN);
Assert.equal(backedAssertion.split("~").length, 2);
Assert.equal(backedAssertion.split(".").length, 3);
});
function test_dsa() {
do_test_pending();
function checkDSA(err, kpo) {
Assert.notEqual(kpo, undefined);
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "DS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.generator, null);
do_check_neq(kpo.prime, null);
do_check_neq(kpo.subPrime, null);
do_check_neq(kpo.publicValue, null);
add_task(async function test_rsa() {
let kpo = await generateKeyPair("RS256");
Assert.notEqual(kpo, undefined);
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "RS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.exponent, null);
do_check_neq(kpo.modulus, null);
let sig = kpo.sign("This is a message to sign");
// TODO: should sign be async?
let sig = kpo.sign("This is a message to sign");
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
do_test_finished();
run_next_test();
}
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
});
jwcrypto.generateKeyPair("DS160", checkDSA);
}
add_task(async function test_dsa() {
let kpo = await generateKeyPair("DS160");
info(kpo.serializedPublicKey);
let pk = JSON.parse(kpo.serializedPublicKey);
Assert.equal(pk.algorithm, "DS");
/* TODO
do_check_neq(kpo.sign, null);
do_check_eq(typeof kpo.sign, "function");
do_check_neq(kpo.userID, null);
do_check_neq(kpo.url, null);
do_check_eq(kpo.url, INTERNAL_ORIGIN);
do_check_neq(kpo.generator, null);
do_check_neq(kpo.prime, null);
do_check_neq(kpo.subPrime, null);
do_check_neq(kpo.publicValue, null);
function test_get_assertion_with_offset() {
do_test_pending();
let sig = kpo.sign("This is a message to sign");
do_check_neq(sig, null);
do_check_eq(typeof sig, "string");
do_check_true(sig.length > 1);
*/
});
add_task(async function test_get_assertion_with_offset() {
// Use an arbitrary date in the past to ensure we don't accidentally pass
// this test with current dates, missing offsets, etc.
let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
@ -135,97 +125,56 @@ function test_get_assertion_with_offset() {
let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
let localMsec = serverMsec - localtimeOffsetMsec;
jwcrypto.generateKeyPair(
"DS160",
function(err, kp) {
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
{ duration: MINUTE_MS,
localtimeOffsetMsec,
now: localMsec},
function(err2, backedAssertion) {
Assert.equal(null, err2);
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
let components = extractComponents(assertion);
// Expiry is within two minutes, corrected for skew
let exp = parseInt(components.payload.exp, 10);
Assert.ok(exp - serverMsec === MINUTE_MS);
do_test_finished();
run_next_test();
}
);
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN,
{
duration: MINUTE_MS,
localtimeOffsetMsec,
now: localMsec,
}
);
}
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
function test_assertion_lifetime() {
do_test_pending();
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
jwcrypto.generateKeyPair(
"DS160",
function(err, kp) {
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
{duration: MINUTE_MS},
function(err2, backedAssertion) {
Assert.equal(null, err2);
let components = extractComponents(assertion);
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
// Expiry is within two minutes, corrected for skew
let exp = parseInt(components.payload.exp, 10);
Assert.ok(exp - serverMsec === MINUTE_MS);
});
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
add_task(async function test_assertion_lifetime() {
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {duration: MINUTE_MS});
// properly formed
let cert;
let assertion;
[cert, assertion] = backedAssertion.split("~");
let components = extractComponents(assertion);
Assert.equal(cert, "fake-cert");
Assert.equal(assertion.split(".").length, 3);
// Expiry is within one minute, as we specified above
let exp = parseInt(components.payload.exp, 10);
Assert.ok(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
Assert.ok(Math.abs(Date.now() - exp) <= MINUTE_MS);
let components = extractComponents(assertion);
do_test_finished();
run_next_test();
}
);
}
);
}
// Expiry is within one minute, as we specified above
let exp = parseInt(components.payload.exp, 10);
Assert.ok(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
Assert.ok(Math.abs(Date.now() - exp) <= MINUTE_MS);
});
function test_audience_encoding_bug972582() {
add_task(async function test_audience_encoding_bug972582() {
let audience = "i-like-pie.com";
jwcrypto.generateKeyPair(
"DS160",
function(err, kp) {
Assert.equal(null, err);
jwcrypto.generateAssertion("fake-cert", kp, audience,
function(err2, backedAssertion) {
Assert.equal(null, err2);
let [/* cert */, assertion] = backedAssertion.split("~");
let components = extractComponents(assertion);
Assert.equal(components.payload.aud, audience);
do_test_finished();
run_next_test();
}
);
}
);
}
// End of tests
// Helper function follow
let kp = await generateKeyPair("DS160");
let backedAssertion = await generateAssertion("fake-cert", kp, audience);
let [/* cert */, assertion] = backedAssertion.split("~");
let components = extractComponents(assertion);
Assert.equal(components.payload.aud, audience);
});
function extractComponents(signedObject) {
if (typeof(signedObject) != "string") {
@ -259,16 +208,3 @@ function extractComponents(signedObject) {
payloadSegment,
cryptoSegment};
}
var TESTS = [
test_sanity,
test_generate,
test_get_assertion,
test_get_assertion_with_offset,
test_assertion_lifetime,
test_audience_encoding_bug972582,
];
TESTS = TESTS.concat([test_rsa, test_dsa]);
TESTS.forEach(f => add_test(f));

View File

@ -11,54 +11,20 @@ function run_test() {
run_next_test();
}
add_test(function test_hawk() {
add_task(async function test_hawk() {
let compute = CryptoUtils.computeHAWK;
// vectors copied from the HAWK (node.js) tests
let credentials_sha1 = {
id: "123456",
key: "2983d45yun89q",
algorithm: "sha1",
};
let method = "POST";
let ts = 1353809207;
let nonce = "Ygvqdz";
let result;
let uri_http = CommonUtils.makeURI("http://example.net/somewhere/over/the/rainbow");
let sha1_opts = { credentials: credentials_sha1,
ext: "Bazinga!",
ts,
nonce,
payload: "something to write about",
};
result = compute(uri_http, method, sha1_opts);
// The HAWK spec uses non-urlsafe base64 (+/) for its output MAC string.
Assert.equal(result.field,
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
'hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", ' +
'mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'
);
Assert.equal(result.artifacts.ts, ts);
Assert.equal(result.artifacts.nonce, nonce);
Assert.equal(result.artifacts.method, method);
Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
Assert.equal(result.artifacts.host, "example.net");
Assert.equal(result.artifacts.port, 80);
// artifacts.hash is the *payload* hash, not the overall request MAC.
Assert.equal(result.artifacts.hash, "bsvY3IfUllw6V5rvk4tStEvpBhE=");
Assert.equal(result.artifacts.ext, "Bazinga!");
let credentials_sha256 = {
let credentials = {
id: "123456",
key: "2983d45yun89q",
algorithm: "sha256",
};
let uri_https = CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow");
let sha256_opts = { credentials: credentials_sha256,
let opts = { credentials,
ext: "Bazinga!",
ts,
nonce,
@ -66,7 +32,7 @@ add_test(function test_hawk() {
contentType: "text/plain",
};
result = compute(uri_https, method, sha256_opts);
let result = await compute(uri_https, method, opts);
Assert.equal(result.field,
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
@ -82,13 +48,13 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=");
Assert.equal(result.artifacts.ext, "Bazinga!");
let sha256_opts_noext = { credentials: credentials_sha256,
let opts_noext = { credentials,
ts,
nonce,
payload: "something to write about",
contentType: "text/plain",
};
result = compute(uri_https, method, sha256_opts_noext);
result = await compute(uri_https, method, opts_noext);
Assert.equal(result.field,
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
@ -108,7 +74,7 @@ add_test(function test_hawk() {
* Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U="
*/
result = compute(uri_https, method, { credentials: credentials_sha256 });
result = await compute(uri_https, method, { credentials });
let fields = result.field.split(" ");
Assert.equal(fields[0], "Hawk");
Assert.equal(fields[1], 'id="123456",'); // from creds.id
@ -122,13 +88,13 @@ add_test(function test_hawk() {
Assert.equal(fields[3].length, ('nonce="12345678901=",').length);
Assert.equal(result.artifacts.nonce.length, ("12345678901=").length);
let result2 = compute(uri_https, method, { credentials: credentials_sha256 });
let result2 = await compute(uri_https, method, { credentials });
Assert.notEqual(result.artifacts.nonce, result2.artifacts.nonce);
/* Using an upper-case URI hostname shouldn't affect the hash. */
let uri_https_upper = CommonUtils.makeURI("https://EXAMPLE.NET/somewhere/over/the/rainbow");
result = compute(uri_https_upper, method, sha256_opts);
result = await compute(uri_https_upper, method, opts);
Assert.equal(result.field,
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
@ -137,7 +103,7 @@ add_test(function test_hawk() {
);
/* Using a lower-case method name shouldn't affect the hash. */
result = compute(uri_https_upper, method.toLowerCase(), sha256_opts);
result = await compute(uri_https_upper, method.toLowerCase(), opts);
Assert.equal(result.field,
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
@ -152,12 +118,12 @@ add_test(function test_hawk() {
* Clients can remember this offset for a while.
*/
result = compute(uri_https, method, { credentials: credentials_sha256,
result = await compute(uri_https, method, { credentials,
now: 1378848968650,
});
Assert.equal(result.artifacts.ts, 1378848968);
result = compute(uri_https, method, { credentials: credentials_sha256,
result = await compute(uri_https, method, { credentials,
now: 1378848968650,
localtimeOffsetMsec: 1000 * 1000,
});
@ -165,23 +131,23 @@ add_test(function test_hawk() {
/* Search/query-args in URIs should be included in the hash. */
let makeURI = CommonUtils.makeURI;
result = compute(makeURI("http://example.net/path"), method, sha256_opts);
result = await compute(makeURI("http://example.net/path"), method, opts);
Assert.equal(result.artifacts.resource, "/path");
Assert.equal(result.artifacts.mac, "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI=");
result = compute(makeURI("http://example.net/path/"), method, sha256_opts);
result = await compute(makeURI("http://example.net/path/"), method, opts);
Assert.equal(result.artifacts.resource, "/path/");
Assert.equal(result.artifacts.mac, "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw=");
result = compute(makeURI("http://example.net/path?query=search"), method, sha256_opts);
result = await compute(makeURI("http://example.net/path?query=search"), method, opts);
Assert.equal(result.artifacts.resource, "/path?query=search");
Assert.equal(result.artifacts.mac, "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho=");
/* Test handling of the payload, which is supposed to be a bytestring
(String with codepoints from U+0000 to U+00FF, pre-encoded). */
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
});
@ -189,8 +155,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=");
// Empty payload changes nothing.
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
payload: null,
@ -198,8 +164,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.hash, undefined);
Assert.equal(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=");
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
payload: "hello",
@ -208,8 +174,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.mac, "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM=");
// update, utf-8 payload
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
payload: "andré@example.org", // non-ASCII
@ -218,8 +184,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=");
/* If "hash" is provided, "payload" is ignored. */
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=",
@ -229,8 +195,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=");
// the payload "hash" is also non-urlsafe base64 (+/)
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
payload: "something else",
@ -243,16 +209,16 @@ add_test(function test_hawk() {
* punycode was a bad joke that got out of the lab and into a spec.
*/
result = compute(makeURI("http://ëxample.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://ëxample.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
});
Assert.equal(result.artifacts.mac, "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0=");
Assert.equal(result.artifacts.host, "xn--xample-ova.net");
result = compute(makeURI("http://example.net/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
ext: "backslash=\\ quote=\" EOF",
@ -260,8 +226,8 @@ add_test(function test_hawk() {
Assert.equal(result.artifacts.mac, "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc=");
Assert.equal(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="');
result = compute(makeURI("http://example.net:1234/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net:1234/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
});
@ -276,15 +242,13 @@ add_test(function test_hawk() {
* updated to do what we do here, so port="01234" should get the same hash
* as port="1234".
*/
result = compute(makeURI("http://example.net:01234/path"), method,
{ credentials: credentials_sha256,
result = await compute(makeURI("http://example.net:01234/path"), method,
{ credentials,
ts: 1353809207,
nonce: "Ygvqdz",
});
Assert.equal(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=");
Assert.equal(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="');
run_next_test();
});

View File

@ -1,152 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.import("resource://services-common/utils.js");
ChromeUtils.import("resource://services-crypto/utils.js");
// Test vectors from RFC 5869
// Test case 1
var tc1 = {
IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt: "000102030405060708090a0b0c",
info: "f0f1f2f3f4f5f6f7f8f9",
L: 42,
PRK: "077709362c2e32df0ddc3f0dc47bba63" +
"90b6c73bb50f9c3122ec844ad7c2b3e5",
OKM: "3cb25f25faacd57a90434f64d0362f2a" +
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
"34007208d5b887185865",
};
// Test case 2
var tc2 = {
IKM: "000102030405060708090a0b0c0d0e0f" +
"101112131415161718191a1b1c1d1e1f" +
"202122232425262728292a2b2c2d2e2f" +
"303132333435363738393a3b3c3d3e3f" +
"404142434445464748494a4b4c4d4e4f",
salt: "606162636465666768696a6b6c6d6e6f" +
"707172737475767778797a7b7c7d7e7f" +
"808182838485868788898a8b8c8d8e8f" +
"909192939495969798999a9b9c9d9e9f" +
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
L: 82,
PRK: "06a6b88c5853361a06104c9ceb35b45c" +
"ef760014904671014a193f40c15fc244",
OKM: "b11e398dc80327a1c8e7f78c596a4934" +
"4f012eda2d4efad8a050cc4c19afa97c" +
"59045a99cac7827271cb41c65e590e09" +
"da3275600c2f09b8367793a9aca3db71" +
"cc30c58179ec3e87c14c01d5c1f3434f" +
"1d87",
};
// Test case 3
var tc3 = {
IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt: "",
info: "",
L: 42,
PRK: "19ef24a32c717b167f33a91d6f648bdf" +
"96596776afdb6377ac434c1c293ccb04",
OKM: "8da4e775a563c18f715f802a063c5a31" +
"b8a11f5c5ee1879ec3454e5f3c738d2d" +
"9d201395faa4b61a96c8",
};
function sha256HMAC(message, key) {
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key);
return CryptoUtils.digestBytes(message, h);
}
function _hexToString(hex) {
let ret = "";
if (hex.length % 2 != 0) {
return false;
}
for (let i = 0; i < hex.length; i += 2) {
let cur = hex[i] + hex[i + 1];
ret += String.fromCharCode(parseInt(cur, 16));
}
return ret;
}
function extract_hex(salt, ikm) {
salt = _hexToString(salt);
ikm = _hexToString(ikm);
return CommonUtils.bytesAsHex(sha256HMAC(ikm, CryptoUtils.makeHMACKey(salt)));
}
function expand_hex(prk, info, len) {
prk = _hexToString(prk);
info = _hexToString(info);
return CommonUtils.bytesAsHex(CryptoUtils.hkdfExpand(prk, info, len));
}
function hkdf_hex(ikm, salt, info, len) {
ikm = _hexToString(ikm);
if (salt)
salt = _hexToString(salt);
info = _hexToString(info);
return CommonUtils.bytesAsHex(CryptoUtils.hkdf(ikm, salt, info, len));
}
// In bug 1437416 we thought we supplied a default for the salt but
// actually ended up calling the platform c++ code with undefined as the
// salt - which still ended up doing the right thing. Let's try and
// codify that behaviour.
let hkdf_tc1 = {
ikm: "foo",
info: "bar",
salt: undefined,
len: 64,
// As all inputs are known, we can pre-calculate the expected result:
// >>> tokenlib.utils.HKDF("foo", None, "bar", 64).encode("hex")
// 'f037f3ab189f485d0d93249f432def681a0305e39ef85f810e2f0b74d2078861fbd34318934b49de822c6148c8bb0785613e4b01176b47634e25eecd5e94ff3b'
result: "f037f3ab189f485d0d93249f432def681a0305e39ef85f810e2f0b74d2078861fbd34318934b49de822c6148c8bb0785613e4b01176b47634e25eecd5e94ff3b",
};
// same inputs, but this time with the default salt explicitly defined.
// should give the same result.
let hkdf_tc2 = {
ikm: "foo",
info: "bar",
salt: String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
len: 64,
result: hkdf_tc1.result,
};
function run_test() {
_("Verifying Test Case 1");
Assert.equal(extract_hex(tc1.salt, tc1.IKM), tc1.PRK);
Assert.equal(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM);
Assert.equal(hkdf_hex(tc1.IKM, tc1.salt, tc1.info, tc1.L), tc1.OKM);
_("Verifying Test Case 2");
Assert.equal(extract_hex(tc2.salt, tc2.IKM), tc2.PRK);
Assert.equal(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM);
Assert.equal(hkdf_hex(tc2.IKM, tc2.salt, tc2.info, tc2.L), tc2.OKM);
_("Verifying Test Case 3");
Assert.equal(extract_hex(tc3.salt, tc3.IKM), tc3.PRK);
Assert.equal(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM);
Assert.equal(hkdf_hex(tc3.IKM, tc3.salt, tc3.info, tc3.L), tc3.OKM);
Assert.equal(hkdf_hex(tc3.IKM, undefined, tc3.info, tc3.L), tc3.OKM);
_("Verifying hkdf semantics");
for (let tc of [hkdf_tc1, hkdf_tc2]) {
let result = CommonUtils.bytesAsHex(CryptoUtils.hkdf(tc.ikm, tc.salt, tc.info, tc.len));
Assert.equal(result, tc.result);
}
}

View File

@ -5,13 +5,12 @@ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://services-common/utils.js");
ChromeUtils.import("resource://services-crypto/utils.js");
function run_test() {
add_test(function setup() {
initTestLogging();
run_next_test();
}
});
add_test(function test_sha1() {
add_task(async function test_sha1() {
_("Ensure HTTP MAC SHA1 generation works as expected.");
let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7";
@ -21,7 +20,7 @@ add_test(function test_sha1() {
let nonce = "wGX71";
let uri = CommonUtils.makeURI("http://10.250.2.176/alias/");
let result = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri,
let result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri,
{ts, nonce});
Assert.equal(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck=");
@ -32,18 +31,16 @@ add_test(function test_sha1() {
let ext = "EXTRA DATA; foo,bar=1";
result = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri,
result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri,
{ts, nonce, ext});
Assert.equal(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68=");
Assert.equal(result.getHeader(),
'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' +
'ext="EXTRA DATA; foo,bar=1"');
run_next_test();
});
add_test(function test_nonce_length() {
add_task(async function test_nonce_length() {
_("Ensure custom nonce lengths are honoured.");
function get_mac(length) {
@ -53,17 +50,15 @@ add_test(function test_nonce_length() {
});
}
let result = get_mac(12);
let result = await get_mac(12);
Assert.equal(12, atob(result.nonce).length);
result = get_mac(2);
result = await get_mac(2);
Assert.equal(2, atob(result.nonce).length);
result = get_mac(0);
result = await get_mac(0);
Assert.equal(8, atob(result.nonce).length);
result = get_mac(-1);
result = await get_mac(-1);
Assert.equal(8, atob(result.nonce).length);
run_next_test();
});

View File

@ -1,156 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.import("resource://services-crypto/utils.js");
ChromeUtils.import("resource://services-common/utils.js");
var {bytesAsHex: b2h} = CommonUtils;
add_task(function test_pbkdf2() {
let symmKey16 = CryptoUtils.pbkdf2Generate("secret phrase", "DNXPzPpiwn", 4096, 16);
Assert.equal(symmKey16.length, 16);
Assert.equal(btoa(symmKey16), "d2zG0d2cBfXnRwMUGyMwyg==");
Assert.equal(CommonUtils.encodeBase32(symmKey16), "O5WMNUO5TQC7LZ2HAMKBWIZQZI======");
let symmKey32 = CryptoUtils.pbkdf2Generate("passphrase", "salt", 4096, 32);
Assert.equal(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) {
Assert.equal(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen)));
}
});
// 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) {
Assert.equal(v.DK,
b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32)));
}
});
// turn formatted test vectors into normal hex strings
function h(hexStr) {
return hexStr.replace(/\s+/g, "");
}

View File

@ -1,37 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
_("Make sure sha1 digests works with various messages");
ChromeUtils.import("resource://services-crypto/utils.js");
function run_test() {
let mes1 = "hello";
let mes2 = "world";
let dig0 = CryptoUtils.UTF8AndSHA1(mes1);
Assert.equal(dig0,
"\xaa\xf4\xc6\x1d\xdc\xc5\xe8\xa2\xda\xbe\xde\x0f\x3b\x48\x2c\xd9\xae\xa9\x43\x4d");
_("Make sure right sha1 digests are generated");
let dig1 = CryptoUtils.sha1(mes1);
Assert.equal(dig1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
let dig2 = CryptoUtils.sha1(mes2);
Assert.equal(dig2, "7c211433f02071597741e6ff5a8ea34789abbf43");
let dig12 = CryptoUtils.sha1(mes1 + mes2);
Assert.equal(dig12, "6adfb183a4a2c94a2f92dab5ade762a47889a5a1");
let dig21 = CryptoUtils.sha1(mes2 + mes1);
Assert.equal(dig21, "5715790a892990382d98858c4aa38d0617151575");
_("Repeated sha1s shouldn't change the digest");
Assert.equal(CryptoUtils.sha1(mes1), dig1);
Assert.equal(CryptoUtils.sha1(mes2), dig2);
Assert.equal(CryptoUtils.sha1(mes1 + mes2), dig12);
Assert.equal(CryptoUtils.sha1(mes2 + mes1), dig21);
_("Nested sha1 should work just fine");
let nest1 = CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(mes1)))));
Assert.equal(nest1, "23f340d0cff31e299158b3181b6bcc7e8c7f985a");
let nest2 = CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(CryptoUtils.sha1(mes2)))));
Assert.equal(nest2, "1f6453867e3fb9876ae429918a64cdb8dc5ff2d0");
}

View File

@ -16,7 +16,4 @@ skip-if = (os == "android" || appname == 'thunderbird')
skip-if = (os == "android" || appname == 'thunderbird')
[test_utils_hawk.js]
[test_utils_hkdfExpand.js]
[test_utils_httpmac.js]
[test_utils_pbkdf2.js]
[test_utils_sha1.js]

View File

@ -23,8 +23,6 @@ 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
@ -52,8 +50,6 @@ var Credentials = Object.freeze({
STRETCHED_PW_LENGTH_BYTES,
HKDF_SALT,
HKDF_LENGTH,
HMAC_ALGORITHM,
HMAC_LENGTH,
},
/**
@ -96,8 +92,6 @@ var Credentials = Object.freeze({
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;
@ -106,18 +100,18 @@ var Credentials = Object.freeze({
let password = CommonUtils.encodeUTF8(passwordInput);
let salt = this.keyWordExtended("quickStretch", emailInput);
let runnable = () => {
let runnable = async () => {
let start = Date.now();
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
let quickStretchedPW = await CryptoUtils.pbkdf2Generate(
password, salt, pbkdf2Rounds, stretchedPWLength);
result.quickStretchedPW = quickStretchedPW;
result.authPW =
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
await CryptoUtils.hkdfLegacy(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
result.unwrapBKey =
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
await CryptoUtils.hkdfLegacy(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
resolve(result);

View File

@ -971,7 +971,7 @@ FxAccountsInternal.prototype = {
const {uid, kB} = userData;
await this.updateUserAccountData({
uid,
...this._deriveKeys(uid, CommonUtils.hexToBytes(kB)),
...(await this._deriveKeys(uid, CommonUtils.hexToBytes(kB))),
kA: null, // Remove kA and kB from storage.
kB: null,
});
@ -1038,7 +1038,7 @@ FxAccountsInternal.prototype = {
log.debug("kBbytes: " + kBbytes);
}
let updateData = {
...this._deriveKeys(data.uid, kBbytes),
...(await this._deriveKeys(data.uid, kBbytes)),
keyFetchToken: null, // null values cause the item to be removed.
unwrapBKey: null,
};
@ -1062,11 +1062,11 @@ FxAccountsInternal.prototype = {
return currentState.resolve(data);
},
_deriveKeys(uid, kBbytes) {
async _deriveKeys(uid, kBbytes) {
return {
kSync: CommonUtils.bytesAsHex(this._deriveSyncKey(kBbytes)),
kSync: CommonUtils.bytesAsHex((await this._deriveSyncKey(kBbytes))),
kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
kExtSync: CommonUtils.bytesAsHex(this._deriveWebExtSyncStoreKey(kBbytes)),
kExtSync: CommonUtils.bytesAsHex((await this._deriveWebExtSyncStoreKey(kBbytes))),
kExtKbHash: CommonUtils.bytesAsHex(this._deriveWebExtKbHash(uid, kBbytes)),
};
},
@ -1077,7 +1077,7 @@ FxAccountsInternal.prototype = {
* @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
*/
_deriveSyncKey(kBbytes) {
return CryptoUtils.hkdf(kBbytes, undefined,
return CryptoUtils.hkdfLegacy(kBbytes, undefined,
"identity.mozilla.com/picl/v1/oldsync", 2 * 32);
},
@ -1087,7 +1087,7 @@ FxAccountsInternal.prototype = {
* @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)
*/
_deriveWebExtSyncStoreKey(kBbytes) {
return CryptoUtils.hkdf(kBbytes, undefined,
return CryptoUtils.hkdfLegacy(kBbytes, undefined,
"identity.mozilla.com/picl/v1/chrome.storage.sync",
2 * 32);
},

View File

@ -187,9 +187,9 @@ this.FxAccountsClient.prototype = {
* @return Promise
* Resolves with a boolean indicating if the session is still valid
*/
sessionStatus(sessionTokenHex) {
return this._request("/session/status", "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken")).then(
async sessionStatus(sessionTokenHex) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
return this._request("/session/status", "GET", credentials).then(
() => Promise.resolve(true),
error => {
if (isInvalidTokenError(error)) {
@ -208,13 +208,13 @@ this.FxAccountsClient.prototype = {
* The session token encoded in hex
* @return Promise
*/
signOut(sessionTokenHex, options = {}) {
async signOut(sessionTokenHex, options = {}) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
let path = "/session/destroy";
if (options.service) {
path += "?service=" + encodeURIComponent(options.service);
}
return this._request(path, "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
return this._request(path, "POST", credentials);
},
/**
@ -224,14 +224,14 @@ this.FxAccountsClient.prototype = {
* The current session token encoded in hex
* @return Promise
*/
recoveryEmailStatus(sessionTokenHex, options = {}) {
async recoveryEmailStatus(sessionTokenHex, options = {}) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
let path = "/recovery_email/status";
if (options.reason) {
path += "?reason=" + encodeURIComponent(options.reason);
}
return this._request(path, "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
return this._request(path, "GET", credentials);
},
/**
@ -241,9 +241,9 @@ this.FxAccountsClient.prototype = {
* The current token encoded in hex
* @return Promise
*/
resendVerificationEmail(sessionTokenHex) {
return this._request("/recovery_email/resend_code", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
async resendVerificationEmail(sessionTokenHex) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
return this._request("/recovery_email/resend_code", "POST", credentials);
},
/**
@ -259,37 +259,36 @@ this.FxAccountsClient.prototype = {
* user's password (bytes)
* }
*/
accountKeys(keyFetchTokenHex) {
let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
async accountKeys(keyFetchTokenHex) {
let creds = await deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
let keyRequestKey = creds.extra.slice(0, 32);
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
let morecreds = await CryptoUtils.hkdfLegacy(keyRequestKey, undefined,
Credentials.keyWord("account/keys"), 3 * 32);
let respHMACKey = morecreds.slice(0, 32);
let respXORKey = morecreds.slice(32, 96);
return this._request("/account/keys", "GET", creds).then(resp => {
if (!resp.bundle) {
throw new Error("failed to retrieve keys");
}
const resp = await this._request("/account/keys", "GET", creds);
if (!resp.bundle) {
throw new Error("failed to retrieve keys");
}
let bundle = CommonUtils.hexToBytes(resp.bundle);
let mac = bundle.slice(-32);
let bundle = CommonUtils.hexToBytes(resp.bundle);
let mac = bundle.slice(-32);
let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
CryptoUtils.makeHMACKey(respHMACKey));
let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
CryptoUtils.makeHMACKey(respHMACKey));
let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
if (mac !== bundleMAC) {
throw new Error("error unbundling encryption keys");
}
let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
if (mac !== bundleMAC) {
throw new Error("error unbundling encryption keys");
}
let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
return {
kA: keyAWrapB.slice(0, 32),
wrapKB: keyAWrapB.slice(32),
};
});
return {
kA: keyAWrapB.slice(0, 32),
wrapKB: keyAWrapB.slice(32),
};
},
/**
@ -308,8 +307,8 @@ this.FxAccountsClient.prototype = {
* wrapping any of these HTTP code/errno pairs:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
*/
signCertificate(sessionTokenHex, serializedPublicKey, lifetime) {
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
async signCertificate(sessionTokenHex, serializedPublicKey, lifetime) {
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { publicKey: serializedPublicKey,
duration: lifetime };
@ -397,10 +396,10 @@ this.FxAccountsClient.prototype = {
* type: Type of device (mobile|desktop)
* }
*/
registerDevice(sessionTokenHex, name, type, options = {}) {
async registerDevice(sessionTokenHex, name, type, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { name, type };
if (options.pushCallback) {
@ -432,7 +431,8 @@ this.FxAccountsClient.prototype = {
* Resolves to an empty object:
* {}
*/
notifyDevices(sessionTokenHex, deviceIds, excludedIds, payload, TTL = 0) {
async notifyDevices(sessionTokenHex, deviceIds, excludedIds, payload, TTL = 0) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
if (deviceIds && excludedIds) {
throw new Error("You cannot specify excluded devices if deviceIds is set.");
}
@ -444,8 +444,7 @@ this.FxAccountsClient.prototype = {
if (excludedIds) {
body.excluded = excludedIds;
}
return this._request("/account/devices/notify", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
return this._request("/account/devices/notify", "POST", credentials, body);
},
/**
@ -457,7 +456,8 @@ this.FxAccountsClient.prototype = {
* had that index will be retrieved.
* @param [limit] - Maximum number of messages to retrieve.
*/
getCommands(sessionTokenHex, {index, limit}) {
async getCommands(sessionTokenHex, {index, limit}) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
const params = new URLSearchParams();
if (index != undefined) {
params.set("index", index);
@ -466,8 +466,7 @@ this.FxAccountsClient.prototype = {
params.set("limit", limit);
}
const path = `/account/device/commands?${params.toString()}`;
return this._request(path, "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
return this._request(path, "GET", credentials);
},
/**
@ -481,14 +480,14 @@ this.FxAccountsClient.prototype = {
* @return Promise
* Resolves to the request's response, (which should be an empty object)
*/
invokeCommand(sessionTokenHex, command, target, payload) {
async invokeCommand(sessionTokenHex, command, target, payload) {
const credentials = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
const body = {
command,
target,
payload,
};
return this._request("/account/devices/invoke_command", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
return this._request("/account/devices/invoke_command", "POST", credentials, body);
},
/**
@ -518,10 +517,10 @@ this.FxAccountsClient.prototype = {
* name: Device name
* }
*/
updateDevice(sessionTokenHex, id, name, options = {}) {
async updateDevice(sessionTokenHex, id, name, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { id, name };
if (options.pushCallback) {
body.pushCallback = options.pushCallback;
@ -554,9 +553,9 @@ this.FxAccountsClient.prototype = {
* ...
* ]
*/
getDeviceList(sessionTokenHex) {
async getDeviceList(sessionTokenHex) {
let path = "/account/devices";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
return this._request(path, "GET", creds, {});
},

View File

@ -1446,13 +1446,13 @@ add_task(async function test_checkVerificationStatusFailed() {
Assert.equal(user.sessionToken, null);
});
add_test(function test_deriveKeys() {
add_task(async function test_deriveKeys() {
let account = MakeFxAccounts();
let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
let kB = CommonUtils.hexToBytes(kBhex);
const uid = "1ad7f502-4cc7-4ec1-a209-071fd2fae348";
const {kSync, kXCS, kExtSync, kExtKbHash} = account.internal._deriveKeys(uid, kB);
const {kSync, kXCS, kExtSync, kExtKbHash} = await account.internal._deriveKeys(uid, kB);
Assert.equal(kSync, "ad501a50561be52b008878b2e0d8a73357778a712255f7722f497b5d4df14b05" +
"dc06afb836e1521e882f521eb34691d172337accdbf6e2a5b968b05a7bbb9885");
@ -1460,7 +1460,6 @@ add_test(function test_deriveKeys() {
Assert.equal(kExtSync, "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
"5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395");
Assert.equal(kExtKbHash, "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273");
run_next_test();
});
/*

View File

@ -33,7 +33,7 @@ add_task(async function test_onepw_setup_credentials() {
let password = CommonUtils.encodeUTF8("i like pie");
let pbkdf2 = CryptoUtils.pbkdf2Generate;
let hkdf = CryptoUtils.hkdf;
let hkdf = CryptoUtils.hkdfLegacy;
// quickStretch the email
let saltyEmail = Credentials.keyWordExtended("quickStretch", email);
@ -43,7 +43,7 @@ add_task(async function test_onepw_setup_credentials() {
let pbkdf2Rounds = 1000;
let pbkdf2Len = 32;
let quickStretchedPW = pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len, Ci.nsICryptoHMAC.SHA256, 32);
let quickStretchedPW = await pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len);
let quickStretchedActual = "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4";
Assert.equal(b2h(quickStretchedPW), quickStretchedActual);
@ -54,13 +54,13 @@ add_task(async function test_onepw_setup_credentials() {
// derive auth password
let hkdfSalt = h2b("00");
let hkdfLen = 32;
let authPW = hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen);
let authPW = await hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen);
Assert.equal(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342");
// derive unwrap key
let unwrapKeyInfo = Credentials.keyWord("unwrapBkey");
let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
let unwrapKey = await hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
Assert.equal(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
});
@ -79,8 +79,6 @@ add_task(async function test_client_stretch_kdf() {
let options = {
stretchedPassLength: 32,
pbkdf2Rounds: 1000,
hmacAlgorithm: Ci.nsICryptoHMAC.SHA256,
hmacLength: 32,
hkdfSalt: h2b("00"),
hkdfLength: 32,
};

View File

@ -601,8 +601,7 @@ this.BrowserIDManager.prototype = {
if (!this._token) {
return null;
}
let credentials = {algorithm: "sha256",
id: this._token.id,
let credentials = {id: this._token.id,
key: this._token.key,
};
method = method || httpObject.method;
@ -615,7 +614,7 @@ this.BrowserIDManager.prototype = {
credentials,
};
let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
let headerValue = await CryptoUtils.computeHAWK(httpObject.uri, method, options);
return {headers: {authorization: headerValue.field}};
},

View File

@ -55,12 +55,10 @@ class HMACMismatch extends Error {
*/
var Utils = {
// Aliases from CryptoUtils.
generateRandomBytes: CryptoUtils.generateRandomBytes,
generateRandomBytesLegacy: CryptoUtils.generateRandomBytesLegacy,
computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1,
digestUTF8: CryptoUtils.digestUTF8,
digestBytes: CryptoUtils.digestBytes,
sha1: CryptoUtils.sha1,
sha1Base32: CryptoUtils.sha1Base32,
sha256: CryptoUtils.sha256,
makeHMACKey: CryptoUtils.makeHMACKey,
makeHMACHasher: CryptoUtils.makeHMACHasher,
@ -192,7 +190,7 @@ var Utils = {
* That makes them 12 characters long with 72 bits of entropy.
*/
makeGUID: function makeGUID() {
return CommonUtils.encodeBase64URL(Utils.generateRandomBytes(9));
return CommonUtils.encodeBase64URL(Utils.generateRandomBytesLegacy(9));
},
_base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i,

View File

@ -65,7 +65,7 @@ add_test(function test_set_invalid_values() {
}
try {
bundle.hmacKey = Utils.generateRandomBytes(15);
bundle.hmacKey = Utils.generateRandomBytesLegacy(15);
} catch (ex) {
thrown = true;
Assert.equal(ex.message.indexOf("HMAC key must be at least 128"), 0);
@ -95,7 +95,7 @@ add_test(function test_set_invalid_values() {
}
try {
bundle.hmacKey = Utils.generateRandomBytes(15);
bundle.hmacKey = Utils.generateRandomBytesLegacy(15);
} catch (ex) {
thrown = true;
Assert.equal(ex.message.indexOf("HMAC key must be at least 128"), 0);

View File

@ -434,7 +434,7 @@ class CryptoCollection {
* @returns {string} A base64-encoded string of the salt
*/
getNewSalt() {
return btoa(CryptoUtils.generateRandomBytes(STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES));
return btoa(CryptoUtils.generateRandomBytesLegacy(STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES));
}
/**

View File

@ -57,12 +57,15 @@ class StaticKeyEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
}
}
const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const STRETCHED_KEY = CryptoUtils.hkdf(BORING_KB, undefined, `testing storage.sync encryption`, 2 * 32);
const KEY_BUNDLE = {
sha256HMACHasher: Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, Utils.makeHMACKey(STRETCHED_KEY.slice(0, 32))),
encryptionKeyB64: btoa(STRETCHED_KEY.slice(32, 64)),
};
const transformer = new StaticKeyEncryptionRemoteTransformer(KEY_BUNDLE);
let transformer;
add_task(async function setup() {
const STRETCHED_KEY = await CryptoUtils.hkdfLegacy(BORING_KB, undefined, `testing storage.sync encryption`, 2 * 32);
const KEY_BUNDLE = {
sha256HMACHasher: Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, Utils.makeHMACKey(STRETCHED_KEY.slice(0, 32))),
encryptionKeyB64: btoa(STRETCHED_KEY.slice(32, 64)),
};
transformer = new StaticKeyEncryptionRemoteTransformer(KEY_BUNDLE);
});
add_task(async function test_encryption_transformer_roundtrip() {
const POSSIBLE_DATAS = [