feat(keys): Add special-case derivation of legacy sync key. (#14); r=vladikoff
This commit is contained in:
Родитель
4ddb967fe6
Коммит
89ba7c75f7
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -54,12 +54,17 @@ return scopedKeys.deriveScopedKey({
|
|||
|
||||
* [deriver-ScopedKeys](#module_deriver-ScopedKeys)
|
||||
* [~deriveScopedKey(options)](#module_deriver-ScopedKeys..deriveScopedKey) ⇒ <code>Promise</code>
|
||||
* [~_deriveLegacySyncKey(options)](#module_deriver-ScopedKeys.._deriveLegacySyncKey) ⇒ <code>Promise</code> ℗
|
||||
* [~_deriveHKDF(salt, initialKeyingMaterial, info, keyLength)](#module_deriver-ScopedKeys.._deriveHKDF) ⇒ <code>Promise</code> ℗
|
||||
|
||||
<a name="module_deriver-ScopedKeys..deriveScopedKey"></a>
|
||||
|
||||
### deriver-ScopedKeys~deriveScopedKey(options) ⇒ <code>Promise</code>
|
||||
Derive a scoped key
|
||||
Derive a scoped key.
|
||||
This method derives the key material for a particular scope from the user's master key material.
|
||||
For most scopes it will produce a JWK containing a 32-byte symmetric key.
|
||||
There is also special-case support for a legacy key-derivation algorithm used by Firefox Sync,
|
||||
which generates a 64-byte key when `options.identifier` is 'https://identity.mozilla.com/apps/oldsync'.
|
||||
|
||||
**Kind**: inner method of [<code>deriver-ScopedKeys</code>](#module_deriver-ScopedKeys)
|
||||
|
||||
|
@ -72,6 +77,20 @@ Derive a scoped key
|
|||
| options.identifier | <code>string</code> | a unique URI string identifying the requested scoped key |
|
||||
| options.uid | <code>string</code> | a 16-byte Firefox Account UID hex string |
|
||||
|
||||
<a name="module_deriver-ScopedKeys.._deriveLegacySyncKey"></a>
|
||||
|
||||
### deriver-ScopedKeys~_deriveLegacySyncKey(options) ⇒ <code>Promise</code> ℗
|
||||
Derive a scoped key using the special legacy algorithm from Firefox Sync.
|
||||
|
||||
**Kind**: inner method of [<code>deriver-ScopedKeys</code>](#module_deriver-ScopedKeys)
|
||||
**Access**: private
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| options | <code>object</code> | required set of options to derive the scoped key |
|
||||
| options.inputKey | <code>string</code> | input key hex string that the scoped key is derived from |
|
||||
| options.keyRotationTimestamp | <code>number</code> | A 13-digit number, the timestamp in milliseconds at which this scoped key most recently changed |
|
||||
|
||||
<a name="module_deriver-ScopedKeys.._deriveHKDF"></a>
|
||||
|
||||
### deriver-ScopedKeys~_deriveHKDF(salt, initialKeyingMaterial, info, keyLength) ⇒ <code>Promise</code> ℗
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
const HKDF = require('node-hkdf');
|
||||
const base64url = require('base64url');
|
||||
const jose = require('node-jose');
|
||||
|
||||
const KEY_LENGTH = 48;
|
||||
const LEGACY_SYNC_SCOPE = 'https://identity.mozilla.com/apps/oldsync';
|
||||
|
||||
/**
|
||||
* Scoped key deriver
|
||||
|
@ -26,7 +28,11 @@ const KEY_LENGTH = 48;
|
|||
*/
|
||||
class ScopedKeys {
|
||||
/**
|
||||
* Derive a scoped key
|
||||
* Derive a scoped key.
|
||||
* This method derives the key material for a particular scope from the user's master key material.
|
||||
* For most scopes it will produce a JWK containing a 32-byte symmetric key.
|
||||
* There is also special-case support for a legacy key-derivation algorithm used by Firefox Sync,
|
||||
* which generates a 64-byte key when `options.identifier` is 'https://identity.mozilla.com/apps/oldsync'.
|
||||
* @method deriveScopedKey
|
||||
* @param {object} options - required set of options to derive a scoped key
|
||||
* @param {string} options.inputKey - input key hex string that the scoped key is derived from
|
||||
|
@ -63,6 +69,10 @@ class ScopedKeys {
|
|||
throw new Error('keyRotationTimestamp must be a 13-digit number');
|
||||
}
|
||||
|
||||
if (options.identifier === LEGACY_SYNC_SCOPE) {
|
||||
return resolve(this._deriveLegacySyncKey(options));
|
||||
}
|
||||
|
||||
const context = 'identity.mozilla.com/picl/v1/scoped_key\n' +
|
||||
options.identifier;
|
||||
const contextBuf = Buffer.from(context);
|
||||
|
@ -74,19 +84,57 @@ class ScopedKeys {
|
|||
scope: options.identifier,
|
||||
};
|
||||
|
||||
this._deriveHKDF(saltBuf, Buffer.concat([inputKeyBuf, keyRotationSecretBuf]), contextBuf, KEY_LENGTH)
|
||||
.then((key) => {
|
||||
const kid = key.slice(0, 16);
|
||||
const k = key.slice(16, 48);
|
||||
const keyTimestamp = Math.round(options.keyRotationTimestamp / 1000);
|
||||
return resolve(
|
||||
this._deriveHKDF(saltBuf, Buffer.concat([inputKeyBuf, keyRotationSecretBuf]), contextBuf, KEY_LENGTH)
|
||||
.then((key) => {
|
||||
const kid = key.slice(0, 16);
|
||||
const k = key.slice(16, 48);
|
||||
const keyTimestamp = Math.round(options.keyRotationTimestamp / 1000);
|
||||
|
||||
scopedKey.k = base64url(k);
|
||||
scopedKey.kid = keyTimestamp + '-' + base64url(kid);
|
||||
scopedKey.k = base64url(k);
|
||||
scopedKey.kid = keyTimestamp + '-' + base64url(kid);
|
||||
|
||||
resolve(scopedKey);
|
||||
});
|
||||
return scopedKey;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a scoped key using the special legacy algorithm from Firefox Sync.
|
||||
* @method _deriveLegacySyncKey
|
||||
* @private
|
||||
* @param {object} options - required set of options to derive the scoped key
|
||||
* @param {string} options.inputKey - input key hex string that the scoped key is derived from
|
||||
* @param {number} options.keyRotationTimestamp
|
||||
* A 13-digit number, the timestamp in milliseconds at which this scoped key most recently changed
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_deriveLegacySyncKey(options) {
|
||||
return new Promise((resolve) => {
|
||||
const context = 'identity.mozilla.com/picl/v1/oldsync';
|
||||
const contextBuf = Buffer.from(context);
|
||||
const inputKeyBuf = Buffer.from(options.inputKey, 'hex');
|
||||
const scopedKey = {
|
||||
kty: 'oct',
|
||||
scope: LEGACY_SYNC_SCOPE
|
||||
};
|
||||
|
||||
return resolve(this._deriveHKDF(null, inputKeyBuf, contextBuf, 64)
|
||||
.then((key) => {
|
||||
scopedKey.k = base64url(key);
|
||||
return jose.JWA.digest('SHA-256', inputKeyBuf)
|
||||
.then((kHash) => {
|
||||
const keyTimestamp = Math.round(options.keyRotationTimestamp / 1000);
|
||||
|
||||
scopedKey.kid = keyTimestamp + '-' + base64url(kHash.slice(0, 16));
|
||||
return scopedKey;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key using HKDF.
|
||||
* Ref: https://tools.ietf.org/html/rfc5869
|
||||
|
|
|
@ -55,6 +55,21 @@ describe('ScopedKeys', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should correctly derive legacy sync key to known test vectors', () => {
|
||||
return scopedKeys.deriveScopedKey({
|
||||
inputKey: 'eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9',
|
||||
keyRotationSecret: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
keyRotationTimestamp: 1510726317000,
|
||||
identifier: 'https://identity.mozilla.com/apps/oldsync',
|
||||
uid: uid
|
||||
})
|
||||
.then((key) => {
|
||||
assert.equal(key.kty, 'oct');
|
||||
assert.equal(key.k, 'DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang');
|
||||
assert.equal(key.kid, '1510726317-IqQv4onc7VcVE1kTQkyyOw');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates keyRotationTimestamp', () => {
|
||||
return scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
|
|
Загрузка…
Ссылка в новой задаче