Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
feat(oauth): Expose relier-specific encryption keys to OAuth WebChann…
Browse files Browse the repository at this point in the history
…el reliers.

OAuth WebChannel reliers may now specify `keys=true` in the URL in order to receive
a setup of relier-specific encryption keys, derived from the account master keys
via HKDF.  The keys will appear as an extra property `keys` on the WebChannel OAuth
result object.

Fixes #2088.
  • Loading branch information
rfk committed Mar 4, 2015
1 parent 0f32c17 commit a0318c2
Show file tree
Hide file tree
Showing 20 changed files with 692 additions and 24 deletions.
3 changes: 2 additions & 1 deletion app/scripts/lib/app-start.js
Expand Up @@ -171,7 +171,7 @@ function (
.then(_.bind(this.initializeProfileClient, this))
// user depends on the profileClient, oAuthClient, and assertionLibrary.
.then(_.bind(this.initializeUser, this))
// broker relies on the user, relier and assertionLibrary
// broker relies on the user, relier, fxaClient and assertionLibrary
.then(_.bind(this.initializeAuthenticationBroker, this))
// the close button depends on the broker
.then(_.bind(this.initializeCloseButton, this))
Expand Down Expand Up @@ -278,6 +278,7 @@ function (
this._authenticationBroker = new WebChannelAuthenticationBroker({
window: this._window,
relier: this._relier,
fxaClient: this._fxaClient,
assertionLibrary: this._assertionLibrary,
oAuthClient: this._oAuthClient,
session: Session
Expand Down
33 changes: 33 additions & 0 deletions app/scripts/lib/base64url.js
@@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

/**
* Base64url encoding/decoding of SJCL bitArrays.
*
* SJCL natively supports standard base64 encoding but not the urlsafe
* variant, which is needed for JWTs.
*/

define([
'sjcl'
], function (sjcl) {

return {

encode: function (bits) {
return sjcl.codec.base64.fromBits(bits)
.replace(/\+/g, '-')
.replace(/\//g, '_');
},

decode: function (chars) {
return sjcl.codec.base64.toBits(
chars.replace(/-/g, '+').replace(/_/g, '/')
);
}

};
});
3 changes: 3 additions & 0 deletions app/scripts/lib/constants.js
Expand Up @@ -24,6 +24,9 @@ define([], function () {

OAUTH_CODE_LENGTH: 64,

RELIER_KEYS_LENGTH: 32,
RELIER_KEYS_CONTEXT_INFO_PREFIX: 'identity.mozilla.com/picl/v1/oauth/',

PASSWORD_MIN_LENGTH: 8,

PROFILE_IMAGE_DISPLAY_SIZE: 240,
Expand Down
23 changes: 12 additions & 11 deletions app/scripts/lib/fxa-client.js
Expand Up @@ -23,14 +23,6 @@ function (_, FxaClient, $, xhr, p, Session, AuthErrors, Constants) {
return $.trim(str);
}

function shouldFetchKeys(relier) {
// isSync is added in case the user verifies in a second tab
// on the first browser, the context will not be available. We
// need to ship the keyFetchToken and unwrapBKey to the first tab,
// so generate these any time we are using sync as well.
return !!(relier.isFxDesktop() || relier.isSync());
}

function FxaClientWrapper(options) {
options = options || {};

Expand Down Expand Up @@ -93,9 +85,11 @@ function (_, FxaClient, $, xhr, p, Session, AuthErrors, Constants) {
verified: accountData.verified || false
};

if (shouldFetchKeys(relier)) {
if (relier.wantsKeys()) {
updatedSessionData.unwrapBKey = accountData.unwrapBKey;
updatedSessionData.keyFetchToken = accountData.keyFetchToken;
}
if (relier.isSync()) {
updatedSessionData.customizeSync = options.customizeSync || false;
}

Expand All @@ -110,7 +104,7 @@ function (_, FxaClient, $, xhr, p, Session, AuthErrors, Constants) {

return self._getClient()
.then(function (client) {
return client.signIn(email, password, { keys: shouldFetchKeys(relier) });
return client.signIn(email, password, { keys: relier.wantsKeys() });
})
.then(function (accountData) {
return self._getUpdatedSessionData(email, relier, accountData, options);
Expand All @@ -128,7 +122,7 @@ function (_, FxaClient, $, xhr, p, Session, AuthErrors, Constants) {
return self._getClient()
.then(function (client) {
var signUpOptions = {
keys: shouldFetchKeys(relier)
keys: relier.wantsKeys()
};

if (relier.has('service')) {
Expand Down Expand Up @@ -363,6 +357,13 @@ function (_, FxaClient, $, xhr, p, Session, AuthErrors, Constants) {
});
},

accountKeys: function (keyFetchToken, unwrapBKey) {
return this._getClient()
.then(function (client) {
return client.accountKeys(keyFetchToken, unwrapBKey);
});
},

// The resume token is eventually for post-verification if the
// user verifies in a second client, with the goal of allowing
// users to continueback to the original RP.
Expand Down
57 changes: 57 additions & 0 deletions app/scripts/lib/hkdf.js
@@ -0,0 +1,57 @@
/* 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/. */
define([
'sjcl',
'lib/promise'
],
function (sjcl, p) {
'use strict';

/**
* hkdf - The HMAC-based Key Derivation Function
* based on https://github.com/mozilla/node-hkdf
*
* @class hkdf
* @param {bitArray} ikm Initial keying material
* @param {bitArray} info Key derivation data
* @param {bitArray} salt Salt
* @param {integer} length Length of the derived key in bytes
* @return promise object- It will resolve with `output` data
*/
function hkdf(ikm, info, salt, length) {

var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
mac.update(ikm);

// compute the PRK
var prk = mac.digest();

// hash length is 32 because only sjcl.hash.sha256 is used at this moment
var hashLength = 32;
var numBlocks = Math.ceil(length / hashLength);
var prev = sjcl.codec.hex.toBits('');
var output = '';

for (var i = 0; i < numBlocks; i++) {
var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256);

var input = sjcl.bitArray.concat(
sjcl.bitArray.concat(prev, info),
sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1)))
);

hmac.update(input);

prev = hmac.digest();
output += sjcl.codec.hex.fromBits(prev);
}

var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8);

return p(truncated);
}

return hkdf;

});
123 changes: 123 additions & 0 deletions app/scripts/lib/relier-keys.js
@@ -0,0 +1,123 @@
/* 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/. */

/**
* Derive relier-specific encryption keys from account master keys.
*/

'use strict';

define([
'sjcl',
'p-promise',
'lib/hkdf',
'lib/base64url',
'lib/constants'
], function (sjcl, p, hkdf, base64url, Constants) {

var KEY_CLASS_TAG_A = 'kAr';
var KEY_CLASS_TAG_B = 'kBr';

/**
* Given the account master keys, the user id and the relier id, generate
* the matching relier-specific derived class-A and class-B keys.
*
* Input arguments:
* keys: object with properties 'kA' and 'kB' giving the account
* keys as hex strings.
* uid: string identifying the user who owns the keys.
* clientId: string identifying the relier for whom keys should
* be derived.
*
* Output:
* A promise that will resolve with an object having 'kAr' and 'kBr'
* properties, giving relier-specific keys derived from 'kA' and 'kB'
* respectively. Each key is represented as a JWK object.
*/
function deriveRelierKeys(keys, uid, clientId) {
var relierKeys = {};
return p()
.then(function () {
if (! keys.kA) {
throw new Error('Cant derive relier keys: missing kA');
}
if (! keys.kB) {
throw new Error('Cant derive relier keys: missing kB');
}
if (! uid) {
throw new Error('Cant derive relier keys: missing uid');
}
if (! clientId) {
throw new Error('Cant derive relier keys: missing rid');
}
return generateDerivedKey({
inputKey: keys.kA,
keyClassTag: KEY_CLASS_TAG_A,
uid: uid,
clientId: clientId
});
}).then(function (kAr) {
relierKeys[KEY_CLASS_TAG_A] = kAr;
return generateDerivedKey({
inputKey: keys.kB,
keyClassTag: KEY_CLASS_TAG_B,
uid: uid,
clientId: clientId
});
}).then(function (kBr) {
relierKeys[KEY_CLASS_TAG_B] = kBr;
return relierKeys;
});
}

/**
* Given master key material, the name of the key class in question,
* the user id and the relier id, generate the appropriate derived key
* as a JWK object.
*
* The key is produced by HKDF-deriving 64 bytes with relier-specific
* HKDF context info. The first 32 bytes are used as an opaque key id
* and the second 32 bytes are the actual key material.
*
* Input options:
* inputKey: hex string giving the master key material.
* keyClassTag: string identifying the type of key to derive; this
* forms part of the HKDF context info and is included
* in the key id.
* uid: string identifying the user who owns the keys.
* clientId: string identifying the relier for whom the key is
* being derived; this forms part of the HKDF context
* info to ensure unique keys for each relier.
*
* Output:
* A promise that will resolve with a JWK object representing the
* derived key.
*/
function generateDerivedKey(options) {
var key = sjcl.codec.hex.toBits(options.inputKey);
var salt = sjcl.codec.hex.toBits('');
var contextInfo = sjcl.codec.utf8String.toBits(
Constants.RELIER_KEYS_CONTEXT_INFO_PREFIX +
options.keyClassTag + ':' + options.clientId
);
return hkdf(key, contextInfo, salt, Constants.RELIER_KEYS_LENGTH * 2)
.then(function (out) {
var keySizeBits = Constants.RELIER_KEYS_LENGTH * 8;
var keyId = sjcl.bitArray.bitSlice(out, 0, keySizeBits);
var keyBytes = sjcl.bitArray.bitSlice(out, keySizeBits, keySizeBits * 2);
return {
kid: options.keyClassTag + '-' + base64url.encode(keyId),
k: base64url.encode(keyBytes),
kty: 'oct',
rid: options.clientId,
uid: options.uid
};
});
}

return {
deriveRelierKeys: deriveRelierKeys,
generateDerivedKey: generateDerivedKey
};
});
37 changes: 37 additions & 0 deletions app/scripts/models/auth_brokers/web-channel.js
Expand Up @@ -25,6 +25,7 @@ define([
initialize: function (options) {
options = options || {};

this._fxaClient = options.fxaClient;
// channel can be passed in for testing.
this._channel = options.channel;

Expand Down Expand Up @@ -54,6 +55,42 @@ define([
return p();
},

/**
* WebChannel reliers can request access to relier-specific encryption
* keys. In the future this logic may be lifted into the base OAuth class
* and made available to all reliers, but we're putting it in this subclass
* for now to guard against accidental exposure.
*
* If the relier indicates that they want keys, the OAuth result will
* get an additional property 'keys', an object containing relier-specific
* keys 'kAr' and 'kBr'.
*/

getOAuthResult: function (account) {
var self = this;
return OAuthAuthenticationBroker.prototype.getOAuthResult.call(this, account)
.then(function (result) {
if (! self.relier.wantsKeys()) {
return result;
}
var uid = account.get('uid');
var keyFetchToken = account.get('keyFetchToken');
var unwrapBKey = account.get('unwrapBKey');
if (! keyFetchToken || ! unwrapBKey) {
result.keys = null;
return result;
}
return self._fxaClient.accountKeys(keyFetchToken, unwrapBKey)
.then(function (keys) {
return self.relier.deriveRelierKeys(keys, uid);
})
.then(function (keys) {
result.keys = keys;
return result;
});
});
},

afterSignIn: function (account) {
return OAuthAuthenticationBroker.prototype.afterSignIn.call(
this, account, { closeWindow: true });
Expand Down
15 changes: 15 additions & 0 deletions app/scripts/models/reliers/base.js
Expand Up @@ -61,6 +61,21 @@ define([
return false;
},

/**
* Check if the relier wants access to the account encryption keys.
*/
wantsKeys: function () {
return false;
},

/**
* Derive relier-specific keys from the account master keys.
* By default no keys are available.
*/
deriveRelierKeys: function (/* keys */) {
return p({});
},

/**
* Create a resume token to be passed along in the email
* verification links
Expand Down
7 changes: 7 additions & 0 deletions app/scripts/models/reliers/fx-desktop.js
Expand Up @@ -62,6 +62,13 @@ define([
return true;
},

/**
* Desktop clients will always want keys so they can sync.
*/
wantsKeys: function () {
return true;
},

_setupServiceName: function () {
var service = this.get('service');
if (service) {
Expand Down

0 comments on commit a0318c2

Please sign in to comment.