зеркало из https://github.com/mozilla/fxa.git
feat: vendor fxa-crypto-relier as crypto-relier
Because: * We want to incorporate the fxa-crypto-relier library into the Firefox Accounts codebase and deprecated its webextension functionality. This commit: * Adds the fxa-crypto-relier library to the Firefox Accounts codebase. Closes FXA-9741
This commit is contained in:
Родитель
a7df9a7458
Коммит
8771178ee3
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.json"],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
# crypto-relier
|
||||
|
||||
This library was vendored from [fxa-crypto-relier](https://github.com/mozilla/fxa-crypto-relier/tree/master)
|
||||
to directly incorporate and improve the code as needed for use in scoped key flows.
|
||||
|
||||
This version has had the OAuthUtils functionality used for webextensions removed and been updated
|
||||
where possible to use the newer jose library.
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build crypto-relier` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test crypto-relier` to execute the unit tests via [Jest](https://jestjs.io).
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'crypto-relier',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/vendored/crypto-relier',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "@fxa/vendored/crypto-relier",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "crypto-relier",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/vendored/crypto-relier/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/vendored/crypto-relier",
|
||||
"tsConfig": "libs/vendored/crypto-relier/tsconfig.lib.json",
|
||||
"packageJson": "libs/vendored/crypto-relier/package.json",
|
||||
"main": "libs/vendored/crypto-relier/src/index.ts",
|
||||
"assets": ["libs/vendored/crypto-relier/*.md"]
|
||||
}
|
||||
},
|
||||
"test-unit": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/vendored/crypto-relier/jest.config.ts",
|
||||
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/* 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/. */
|
||||
|
||||
export * from './lib/deriver';
|
||||
export * from './lib/relier';
|
|
@ -0,0 +1,44 @@
|
|||
/* 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/. */
|
||||
|
||||
import * as jose from 'jose';
|
||||
|
||||
/**
|
||||
* Scoped key deriver utilities
|
||||
* @module deriver-DeriverUtils
|
||||
* @private
|
||||
*/
|
||||
export class DeriverUtils {
|
||||
/**
|
||||
* @method encryptBundle
|
||||
* @param {string} appPublicKeyJwk - base64url encoded string of the public key JWK
|
||||
* @param {string} bundle - String bundle to encrypt using the provided key
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async encryptBundle(appPublicKeyJwk: string, bundle: string) {
|
||||
const rawKey = jose.decodeJwt('.' + appPublicKeyJwk + '.');
|
||||
const key = await jose.importJWK(rawKey);
|
||||
|
||||
// To help reliers do the right thing, we reject keys that aren't exactly as we expect.
|
||||
// In the future we might open up to additional key types, but for now it's better to
|
||||
// be strict in what we accept.
|
||||
if (rawKey.kty !== 'EC') {
|
||||
throw new Error('appJwk is not an EC key');
|
||||
}
|
||||
if (rawKey.crv !== 'P-256') {
|
||||
throw new Error('appJwk is not on curve P-256');
|
||||
}
|
||||
if ('d' in rawKey) {
|
||||
throw new Error('appJwk includes the private key');
|
||||
}
|
||||
|
||||
return new jose.CompactEncrypt(new TextEncoder().encode(bundle))
|
||||
.setProtectedHeader({
|
||||
alg: 'ECDH-ES',
|
||||
enc: 'A256GCM',
|
||||
kid: rawKey.kid as any,
|
||||
})
|
||||
.encrypt(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/* 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/. */
|
||||
|
||||
import * as jose from 'jose';
|
||||
|
||||
import { DeriverUtils } from './deriver-utils';
|
||||
|
||||
describe('DeriverUtils', () => {
|
||||
const deriverUtils = new DeriverUtils();
|
||||
const b64urlencode = jose.base64url.encode;
|
||||
|
||||
const exampleScope = 'https://identity.mozilla.com/apps/notes';
|
||||
const keySample = {
|
||||
[exampleScope]: {
|
||||
kty: 'oct',
|
||||
scope: exampleScope,
|
||||
k: 'XQzv2cjJfSMsi3NPn0nVVWprUbhlVvuOBkyEqwvjMdk',
|
||||
kid: '20171004201318-jcbS5axUtJCRK3Rc-5rj4fsLhh3LOENEIFGwrau2bjI',
|
||||
},
|
||||
};
|
||||
|
||||
describe('encryptBundle', () => {
|
||||
it('can encrypt the bundle', async () => {
|
||||
const appPublicKeyJwk =
|
||||
'eyJrdHkiOiJFQyIsImtpZCI6ImduUGtGWjE2dHNyeTFsajdDUHdXaENxVkxPSGwtMXFETmJIbG5FNTJzOVEiLCJjcnYiOiJQLTI1NiIsIngiOiJFS3lFOWRta3U2aTNhclpOVVBqdkl0bmo2V2pPUzBldzdENkZQaDR2OFFZIiwieSI6IjRhX3VHenM2Rl9uN0ZrNTZIaDlUZGlMZHNjblg4UHdjTnlXZ3lqeG9td0kifQ';
|
||||
|
||||
const enc = await deriverUtils.encryptBundle(
|
||||
appPublicKeyJwk,
|
||||
JSON.stringify(keySample)
|
||||
);
|
||||
expect(enc.length).toBe(632);
|
||||
});
|
||||
|
||||
it('rejects keys that include the private key component', async () => {
|
||||
const appPublicKeyJwk = b64urlencode(
|
||||
JSON.stringify({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
d: 'KXAjjEr4KT9UlYI4BE0BefVdoxP8vqO389U7lQlCigs',
|
||||
x: 'SiBn6uebjigmQqw4TpNzs3AUyCae1_sG2b9Fzhq3Fyo',
|
||||
y: 'q99Xq1RWNTFpk99pdQOSjUvwELss51PkmAGCXhLfMV4',
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
deriverUtils.encryptBundle(appPublicKeyJwk, JSON.stringify(keySample))
|
||||
).rejects.toThrow('appJwk includes the private key');
|
||||
});
|
||||
|
||||
it('rejects symmetric keys', async () => {
|
||||
const appPublicKeyJwk = b64urlencode(
|
||||
JSON.stringify({
|
||||
kty: 'oct',
|
||||
k: 'U4ObmO4YmLfHLqYgFd9Q2Q',
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
deriverUtils.encryptBundle(appPublicKeyJwk, JSON.stringify(keySample))
|
||||
).rejects.toThrow('appJwk is not an EC key');
|
||||
});
|
||||
|
||||
it('rejects non-ECDH public keys', async () => {
|
||||
const appPublicKeyJwk = b64urlencode(
|
||||
JSON.stringify({
|
||||
kty: 'RSA',
|
||||
e: 'AQAB',
|
||||
n: 'nV-WzW3lHd03yEUG88M-r_F0WwCKhlv4O5Yxu5QNiOQnDdDvGwpWTZMeBz9iAtu2S_cia-woK2XcTBOnSorNcC_2YA44aJtBK2TnLR_Ks6Tru2QzO95uDKI7U8mQdUhU_66aCCHTtr5AK178Z29sKoqabivIj3tHnDSLiZSpQgZkJP-jCXat5JyRC2rU6eFX9mORLIkpIyXQxdz_WSSg5DYMhJ20EWoIfMODFIZS4H-w3aYkhv7Ao2dmwozq2iwYsNLmZA26uXdYbqUvpi6kjQZusmPk1OD6E-TnjKh1qI3fi5XesdIf4b-N8fTDwhub6-Vgdh1-8biWmXVFZjc2iQ',
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
deriverUtils.encryptBundle(appPublicKeyJwk, JSON.stringify(keySample))
|
||||
).rejects.toThrow('appJwk is not an EC key');
|
||||
});
|
||||
|
||||
it('rejects keys on curves other than P-256', async () => {
|
||||
const appPublicKeyJwk = b64urlencode(
|
||||
JSON.stringify({
|
||||
kty: 'EC',
|
||||
crv: 'P-384',
|
||||
x: 'Txvn927uYdiqgSRtHgX3aTVH1_3bMyDM08yN-SRF7Q-2wouLoI70vawCO8i2UaAv',
|
||||
y: '38oIUqk9a6qtAyq25PAvxwApdPcHg6RaXN3Du70E3sIHKbGtXBX0KBbcFh4yYKUu',
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
deriverUtils.encryptBundle(appPublicKeyJwk, JSON.stringify(keySample))
|
||||
).rejects.toThrow('appJwk is not on curve P-256');
|
||||
});
|
||||
|
||||
it('rejects public keys whose points are not on the curve', async () => {
|
||||
const appPublicKeyJwk = b64urlencode(
|
||||
JSON.stringify({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
x: 'SiBn6uebjigmQqw4TpNzs3AUyCae1_sG2b9Fzhq3Fyo',
|
||||
y: 'q99Xq1RWNTFpk99pdQOSjUvwELss51PkmAGCXhLfMV3',
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
deriverUtils.encryptBundle(appPublicKeyJwk, JSON.stringify(keySample))
|
||||
).rejects.toThrow('Invalid JWK EC key');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/* 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/. */
|
||||
|
||||
export * as jose from 'jose';
|
||||
export * as base64url from 'base64url';
|
||||
export * as DeriverUtils from './deriver-utils';
|
||||
export * as ScopedKeys from './scoped-keys';
|
|
@ -0,0 +1,448 @@
|
|||
/* 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/. */
|
||||
|
||||
import { subtle } from 'crypto';
|
||||
|
||||
import { ScopedKeys } from './scoped-keys';
|
||||
|
||||
describe('ScopedKeys', function () {
|
||||
const scopedKeys = new ScopedKeys();
|
||||
const sampleKb =
|
||||
'bc3851e9e610f631df94d7883d5defd5e5f55ab520bd5a9ae33dae26575c6b1a';
|
||||
const identifier = 'https://identity.mozilla.com/apps/notes';
|
||||
const keyRotationTimestamp = 1494446722583; // GMT Wednesday, May 10, 2017 8:05:22.583 PM
|
||||
const keyRotationSecret =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const uid = 'aeaa1725c7a24ff983c6295725d5fc9b';
|
||||
|
||||
it('should have HKDF work', async () => {
|
||||
const key = await scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
const importSpec = {
|
||||
name: 'AES-CTR',
|
||||
};
|
||||
|
||||
expect(key.kty).toBe('oct');
|
||||
expect(key.k).toBe('b9SaftekqPfXDOEeZx8714ktkcG9mBbNnKIyoUmhShE');
|
||||
expect(key.kid).toBe('1494446723-sXWpWP2sQkP-KsjIPGm1gg');
|
||||
expect(key.scope).toBe(identifier);
|
||||
const rawKey = await subtle.importKey('jwk', key, importSpec, false, [
|
||||
'encrypt',
|
||||
]);
|
||||
expect(rawKey.type).toBe('secret');
|
||||
expect(rawKey.usages[0]).toBe('encrypt');
|
||||
expect(rawKey.extractable).toBe(false);
|
||||
});
|
||||
|
||||
it('should match the output of test vectors generated via python script', async () => {
|
||||
const key = await scopedKeys.deriveScopedKey({
|
||||
inputKey:
|
||||
'8b2e1303e21eee06a945683b8d495b9bf079ca30baa37eb8392d9ffa4767be45',
|
||||
keyRotationSecret:
|
||||
'517d478cb4f994aa69930416648a416fdaa1762c5abf401a2acf11a0f185e98d',
|
||||
keyRotationTimestamp: 1510726317000,
|
||||
identifier: 'app_key:https%3A//example.com',
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
expect(key.kty).toBe('oct');
|
||||
expect(key.k).toBe('Kkbk1_Q0oCcTmggeDH6880bQrxin2RLu5D00NcJazdQ');
|
||||
expect(key.kid).toBe('1510726317-Voc-Eb9IpoTINuo9ll7bjA');
|
||||
});
|
||||
|
||||
it('should correctly derive legacy sync key to known test vectors', async () => {
|
||||
const key = await scopedKeys.deriveScopedKey({
|
||||
inputKey:
|
||||
'eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9',
|
||||
keyRotationSecret:
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
keyRotationTimestamp: 1510726317123,
|
||||
identifier: 'https://identity.mozilla.com/apps/oldsync',
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
expect(key.kty).toBe('oct');
|
||||
expect(key.k).toBe(
|
||||
'DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang'
|
||||
);
|
||||
expect(key.kid).toBe('1510726317123-IqQv4onc7VcVE1kTQkyyOw');
|
||||
});
|
||||
|
||||
it('should correctly derive Thunderbird sync key to known test vectors', async () => {
|
||||
const key = await scopedKeys.deriveScopedKey({
|
||||
inputKey:
|
||||
'eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9',
|
||||
keyRotationSecret:
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
keyRotationTimestamp: 1715043913541,
|
||||
identifier: 'https://identity.thunderbird.net/apps/sync',
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
expect(key.kty).toBe('oct');
|
||||
expect(key.k).toBe(
|
||||
'DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang'
|
||||
);
|
||||
expect(key.kid).toBe('1715043913541-IqQv4onc7VcVE1kTQkyyOw');
|
||||
});
|
||||
|
||||
it('validates that inputKey is provided', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
} as any)
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'inputKey must be a 64-character hex string'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that inputKey is a hex string', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
inputKey: 'k' + sampleKb.slice(1),
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
})
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'inputKey must be a 64-character hex string'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that inputKey has the required length', async () => {
|
||||
try {
|
||||
await scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb.slice(0, 16),
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('inputKey must be a 64-character hex string');
|
||||
}
|
||||
});
|
||||
|
||||
it('validates that keyRotationSecret is provided', async () => {
|
||||
try {
|
||||
await scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
} as any);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
'keyRotationSecret must be a 64-character hex string'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validates that keyRotationSecret is a hex string', async () => {
|
||||
try {
|
||||
await scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: 'Q' + keyRotationSecret.slice(1),
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
'keyRotationSecret must be a 64-character hex string'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validates that keyRotationSecret has the required length', async () => {
|
||||
try {
|
||||
await scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret.slice(0, 16),
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
'keyRotationSecret must be a 64-character hex string'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validates that keyRotationTimestamp is provided', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
} as any)
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'keyRotationTimestamp must be a 13-digit integer'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that keyRotationTimestamp is a number', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: '1111111111111',
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
} as any)
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'keyRotationTimestamp must be a 13-digit integer'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that keyRotationTimestamp is an integer', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: 1234567890.23,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
})
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'keyRotationTimestamp must be a 13-digit integer'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that keyRotationTimestamp has correct number of digits', async () => {
|
||||
await expect(
|
||||
scopedKeys.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: 100,
|
||||
identifier: identifier,
|
||||
uid: uid,
|
||||
})
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
'keyRotationTimestamp must be a 13-digit integer'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates that identifier is provided', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
uid: uid,
|
||||
} as any)
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('identifier must be a string of length >= 10');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates that identifier is a string', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: true,
|
||||
uid: uid,
|
||||
} as any)
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('identifier must be a string of length >= 10');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates that identifier is of non-trivial length', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: 'https://x',
|
||||
uid: uid,
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('identifier must be a string of length >= 10');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates that uid is provided', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
} as any)
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('uid must be a 32-character hex string');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates that uid is a hex string', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: '!' + uid.slice(1),
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('uid must be a 32-character hex string');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates that uid has the correct length', () => {
|
||||
expect.assertions(1);
|
||||
return scopedKeys
|
||||
.deriveScopedKey({
|
||||
inputKey: sampleKb,
|
||||
keyRotationSecret: keyRotationSecret,
|
||||
keyRotationTimestamp: keyRotationTimestamp,
|
||||
identifier: identifier,
|
||||
uid: uid.slice(0, 16),
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.message).toBe('uid must be a 32-character hex string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_deriveHKDF', () => {
|
||||
it('vector 1', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from(
|
||||
'000102030405060708090a0b0c',
|
||||
'hex'
|
||||
);
|
||||
const context = Buffer.from('f0f1f2f3f4f5f6f7f8f9', 'hex');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
42
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865'
|
||||
);
|
||||
});
|
||||
|
||||
it('vector 2', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from('');
|
||||
const context = Buffer.from('');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
42
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8'
|
||||
);
|
||||
});
|
||||
|
||||
it('vector 3', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'4a9cbe5ae7190a7bb7cc54d5d84f5e4ba743904f8a764933b72f10260067375a',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from('');
|
||||
const context = Buffer.from('identity.mozilla.com/picl/v1/keyFetchToken');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
3 * 32
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'f4df04ffb79db35e94e4881719a6f145f9206e8efea17fc9f02a5ce09cbfac1e829a935f34111d75e0d16b7aa178e2766759eedb6f623c0babd2abcfea82bc12af75f6aa543a8ba7e0a029f87c785c4af0ad03889f7437f735b5256a88fc73fd'
|
||||
);
|
||||
});
|
||||
|
||||
it('vector 4', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from('');
|
||||
const context = Buffer.from('identity.mozilla.com/picl/v1/account/keys');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
3 * 32
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be1599975702285de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85'
|
||||
);
|
||||
});
|
||||
|
||||
it('vector 5', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'bc3851e9e610f631df94d7883d5defd5e5f55ab520bd5a9ae33dae26575c6b1a',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from(
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'hex'
|
||||
);
|
||||
const context = Buffer.from('https://identity.mozilla.com/apps/notes');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
32
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'989131d32cd665c26a57cf9ece14d0e5cf015834e9d2916d683a3bb486ceb06f'
|
||||
);
|
||||
});
|
||||
|
||||
it('vector 6', async () => {
|
||||
const inputKey = Buffer.from(
|
||||
'bc3851e9e610f631df94d7883d5defd5e5f55ab520bd5a9ae33dae26575c6b1a',
|
||||
'hex'
|
||||
);
|
||||
const keyRotationSecret = Buffer.from('');
|
||||
const context = Buffer.from('https://identity.mozilla.com/apps/notes');
|
||||
const key = await scopedKeys._deriveHKDF(
|
||||
keyRotationSecret,
|
||||
inputKey,
|
||||
context,
|
||||
32
|
||||
);
|
||||
expect(key.toString('hex')).toBe(
|
||||
'989131d32cd665c26a57cf9ece14d0e5cf015834e9d2916d683a3bb486ceb06f'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
/* 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/. */
|
||||
|
||||
import { subtle } from 'crypto';
|
||||
import * as jose from 'jose';
|
||||
import HKDF from 'node-hkdf';
|
||||
|
||||
const KEY_LENGTH = 48;
|
||||
const SYNC_SCOPES = [
|
||||
'https://identity.mozilla.com/apps/oldsync',
|
||||
'https://identity.thunderbird.net/apps/sync',
|
||||
];
|
||||
const REGEX_HEX32 = /^[0-9a-f]{32}$/i;
|
||||
const REGEX_HEX64 = /^[0-9a-f]{64}$/i;
|
||||
const REGEX_TIMESTAMP = /^[0-9]{13}$/;
|
||||
|
||||
/**
|
||||
* Scoped key deriver
|
||||
* @desc Used by the Firefox Accounts content server
|
||||
* @module deriver-ScopedKeys
|
||||
* @example
|
||||
* ```js
|
||||
* const scopedKeys = new fxaCryptoDeriver.ScopedKeys();
|
||||
*
|
||||
* return scopedKeys.deriveScopedKey({
|
||||
* identifier: 'https://identity.mozilla.com/apps/notes',
|
||||
* inputKey: 'bc3851e9e610f631df94d7883d5defd5e5f55ab520bd5a9ae33dae26575c6b1a',
|
||||
* keyRotationSecret: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
* keyRotationTimestamp: 1494446722583,
|
||||
* uid: 'aeaa1725c7a24ff983c6295725d5fc9b'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ScopedKeys {
|
||||
/**
|
||||
* 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
|
||||
* @param {string} options.keyRotationSecret - a 32-byte hex string of additional entropy specific to this scoped key
|
||||
* @param {number} options.keyRotationTimestamp
|
||||
* A 13-digit number, the timestamp in milliseconds at which this scoped key most recently changed
|
||||
* @param {string} options.identifier - a unique URI string identifying the requested scoped key
|
||||
* @param {string} options.uid - a 16-byte Firefox Account UID hex string
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async deriveScopedKey(options: {
|
||||
identifier: string;
|
||||
inputKey: string;
|
||||
keyRotationSecret: string;
|
||||
keyRotationTimestamp: number;
|
||||
uid: string;
|
||||
}) {
|
||||
if (!REGEX_HEX64.test(options.inputKey)) {
|
||||
throw new Error('inputKey must be a 64-character hex string');
|
||||
}
|
||||
|
||||
if (!REGEX_HEX64.test(options.keyRotationSecret)) {
|
||||
throw new Error('keyRotationSecret must be a 64-character hex string');
|
||||
}
|
||||
|
||||
if (typeof options.keyRotationTimestamp !== 'number') {
|
||||
throw new Error('keyRotationTimestamp must be a 13-digit integer');
|
||||
}
|
||||
|
||||
if (!REGEX_TIMESTAMP.test(options.keyRotationTimestamp.toString())) {
|
||||
throw new Error('keyRotationTimestamp must be a 13-digit integer');
|
||||
}
|
||||
|
||||
if (
|
||||
typeof options.identifier !== 'string' ||
|
||||
options.identifier.length < 10
|
||||
) {
|
||||
throw new Error('identifier must be a string of length >= 10');
|
||||
}
|
||||
|
||||
if (!REGEX_HEX32.test(options.uid)) {
|
||||
throw new Error('uid must be a 32-character hex string');
|
||||
}
|
||||
|
||||
if (SYNC_SCOPES.includes(options.identifier)) {
|
||||
return this._deriveLegacySyncKey(options);
|
||||
}
|
||||
|
||||
const context =
|
||||
'identity.mozilla.com/picl/v1/scoped_key\n' + options.identifier;
|
||||
const contextBuf = Buffer.from(context);
|
||||
const inputKeyBuf = Buffer.from(options.inputKey, 'hex');
|
||||
const keyRotationSecretBuf = Buffer.from(options.keyRotationSecret, 'hex');
|
||||
const saltBuf = Buffer.from(options.uid, 'hex');
|
||||
const scopedKey: Record<string, string> = {
|
||||
kty: 'oct',
|
||||
scope: options.identifier,
|
||||
};
|
||||
|
||||
const key = await this._deriveHKDF(
|
||||
saltBuf,
|
||||
Buffer.concat([inputKeyBuf, keyRotationSecretBuf]),
|
||||
contextBuf,
|
||||
KEY_LENGTH
|
||||
);
|
||||
const kid = key.slice(0, 16);
|
||||
const k = key.slice(16, 48);
|
||||
const keyTimestamp = Math.round(options.keyRotationTimestamp / 1000);
|
||||
|
||||
scopedKey.k = jose.base64url.encode(k);
|
||||
scopedKey.kid = keyTimestamp + '-' + jose.base64url.encode(kid);
|
||||
|
||||
return scopedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a scoped key using the special legacy algorithm from Firefox Sync.
|
||||
* To access data in Firefox Sync, clients need to know:
|
||||
* * 64 bytes of key material derived from kB using HKDF
|
||||
* * The first 16 bytes of the SHA-256 hash of kB
|
||||
* * The full millisecond precision timestamp of when kB was last changed.
|
||||
* This method encodes that information as a JWK by using the first as the
|
||||
* key material `k`, and combining the other two to form the `kid`.
|
||||
* @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}
|
||||
*/
|
||||
async _deriveLegacySyncKey(options: {
|
||||
identifier: string;
|
||||
inputKey: string;
|
||||
keyRotationTimestamp: number;
|
||||
}) {
|
||||
const context = 'identity.mozilla.com/picl/v1/oldsync';
|
||||
const contextBuf = Buffer.from(context);
|
||||
const inputKeyBuf = Buffer.from(options.inputKey, 'hex');
|
||||
const scopedKey: Record<string, string> = {
|
||||
kty: 'oct',
|
||||
scope: options.identifier,
|
||||
};
|
||||
|
||||
const key = await this._deriveHKDF(
|
||||
Buffer.from(''),
|
||||
inputKeyBuf,
|
||||
contextBuf,
|
||||
64
|
||||
);
|
||||
scopedKey.k = jose.base64url.encode(Buffer.from(key));
|
||||
|
||||
const kHash = await subtle.digest('SHA-256', Buffer.from(inputKeyBuf));
|
||||
scopedKey.kid =
|
||||
options.keyRotationTimestamp +
|
||||
'-' +
|
||||
jose.base64url.encode(Buffer.from(kHash.slice(0, 16)));
|
||||
return scopedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key using HKDF.
|
||||
* Ref: https://tools.ietf.org/html/rfc5869
|
||||
* @method _deriveHKDF
|
||||
* @private
|
||||
* @param {buffer} salt
|
||||
* @param {buffer} initialKeyingMaterial
|
||||
* @param {buffer} info
|
||||
* @param {number} keyLength - Key length
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async _deriveHKDF(
|
||||
salt: Buffer,
|
||||
initialKeyingMaterial: Buffer,
|
||||
info: Buffer,
|
||||
keyLength: number
|
||||
): Promise<Buffer> {
|
||||
return new Promise((resolve) => {
|
||||
// Safari doesn't have HKDF yet in their Web Crypto API
|
||||
const hkdf = new HKDF('sha256', salt, initialKeyingMaterial);
|
||||
|
||||
hkdf.derive(info, keyLength, (key: any) => {
|
||||
return resolve(key);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
export * from './key-utils';
|
|
@ -0,0 +1,83 @@
|
|||
/* 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/. */
|
||||
|
||||
import * as jose from 'node-jose';
|
||||
|
||||
import { DeriverUtils } from '../deriver/deriver-utils';
|
||||
import { KeyUtils } from './key-utils';
|
||||
|
||||
describe('KeyUtils', function () {
|
||||
describe('createApplicationKeyPair', () => {
|
||||
it('should output a JWK public key', () => {
|
||||
const keyUtils = new KeyUtils();
|
||||
|
||||
return keyUtils.createApplicationKeyPair().then((result) => {
|
||||
const jwk = result.jwkPublicKey;
|
||||
expect(jwk.kty).toBe('EC');
|
||||
expect(jwk.kid.length).toBe(43);
|
||||
expect(jwk.crv).toBe('P-256');
|
||||
expect(jwk.x.length).toBe(43);
|
||||
expect(jwk.y.length).toBe(43);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptBundle', () => {
|
||||
it('fails with no key store', async () => {
|
||||
const keyUtils = new KeyUtils();
|
||||
|
||||
return expect(keyUtils.decryptBundle('bundle')).rejects.toThrowError(
|
||||
'No Key Store. Use .createApplicationKeyPair() to create it first.'
|
||||
);
|
||||
});
|
||||
|
||||
it('can decrypt a bundle', async () => {
|
||||
const derivedKeys = {
|
||||
'https://identity.mozilla.com/apps/notes': {
|
||||
kid: '<opaque key identifier>',
|
||||
k: '<notes encryption key, b64url-encoded>',
|
||||
kty: 'oct',
|
||||
},
|
||||
};
|
||||
const keyUtils = new KeyUtils();
|
||||
const deriverUtils = new DeriverUtils();
|
||||
|
||||
const keys = await keyUtils.createApplicationKeyPair();
|
||||
const base64JwkPublicKey = jose.util.base64url.encode(
|
||||
JSON.stringify(keys.jwkPublicKey),
|
||||
'utf8'
|
||||
);
|
||||
const encryptedBundle = await deriverUtils.encryptBundle(
|
||||
base64JwkPublicKey,
|
||||
JSON.stringify(derivedKeys)
|
||||
);
|
||||
const decryptedBundle = await keyUtils.decryptBundle(encryptedBundle);
|
||||
expect(decryptedBundle).toEqual(derivedKeys);
|
||||
});
|
||||
|
||||
it('can decrypt a test vector key bundle generated via python code', async () => {
|
||||
const keyUtils = new KeyUtils();
|
||||
keyUtils.keystore = jose.JWK.createKeyStore();
|
||||
await keyUtils.keystore.add({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
d: 'KXAjjEr4KT9UlYI4BE0BefVdoxP8vqO389U7lQlCigs',
|
||||
x: 'SiBn6uebjigmQqw4TpNzs3AUyCae1_sG2b9Fzhq3Fyo',
|
||||
y: 'q99Xq1RWNTFpk99pdQOSjUvwELss51PkmAGCXhLfMV4',
|
||||
});
|
||||
|
||||
const result = await keyUtils.decryptBundle(
|
||||
'eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ik40elBSYXpCODd2cGVCZ0h6RnZrdmRfNDhvd0ZZWXhFVlhSTXJPVTZMRG8iLCJ5IjoiNG5jVXhONnhfeFQxVDFrenlfU19WMmZZWjd1VUpUX0hWUk5aQkxKUnN4VSJ9fQ.._0sYf7HdWuRv2cM0.U5ZK5BYZWhLluS7q4y4ZFW1UwcW-s7mjuY_khe4OVjtvLE5jOQw-qGyT_06wY2zpqN6FhGMa16Qhn4UABz0LuDwAfrHOtfRlpqeV3nrKhas2gXt1yLvDFLide4hEPfBJk60t2CXjxprsA1BulinIER2EIJbA.rpf5rzO78Hj-9CWRTLx7TQ'
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
app_key: {
|
||||
k: 'rTcZ5olrrJWqVD6bVtLjHJT0P6d_9IdpEgWT4zVzMb0',
|
||||
kid: '1510726317-UvyHCg_RD3zKl_hQdlRsfw',
|
||||
kty: 'oct',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/* 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/. */
|
||||
|
||||
import * as jose from 'node-jose';
|
||||
|
||||
/**
|
||||
* Scoped key utilities
|
||||
* @module relier-KeyUtils
|
||||
* @private
|
||||
*/
|
||||
export class KeyUtils {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor(public keystore: any = null) {}
|
||||
/**
|
||||
* @method createApplicationKeyPair
|
||||
* @desc Returns a JWK public key
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async createApplicationKeyPair() {
|
||||
const keystore = jose.JWK.createKeyStore();
|
||||
const keyPair = await keystore.generate('EC', 'P-256');
|
||||
this.keystore = keystore;
|
||||
|
||||
return {
|
||||
jwkPublicKey: keyPair.toJSON(),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @method decryptBundle
|
||||
* @desc Decrypts a given bundle using the JWK key store
|
||||
* @param {string} bundle
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async decryptBundle(bundle: string) {
|
||||
if (!this.keystore) {
|
||||
throw new Error(
|
||||
'No Key Store. Use .createApplicationKeyPair() to create it first.'
|
||||
);
|
||||
}
|
||||
|
||||
const decryptedBundle = await jose.JWE.createDecrypt(this.keystore).decrypt(
|
||||
bundle
|
||||
);
|
||||
return JSON.parse(jose.util.utf8.encode(decryptedBundle.plaintext));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -80,6 +80,7 @@
|
|||
"@swc/helpers": "~0.5.2",
|
||||
"@type-cacheable/core": "^14.0.1",
|
||||
"@type-cacheable/ioredis-adapter": "^10.0.4",
|
||||
"base64url": "^3.0.1",
|
||||
"bn.js": "^5.2.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"graphql-request": "^6.1.0",
|
||||
"hot-shots": "^10.0.0",
|
||||
"husky": "^4.2.5",
|
||||
"jose": "^5.3.0",
|
||||
"knex": "^3.1.0",
|
||||
"kysely": "^0.27.2",
|
||||
"lint-staged": "^15.2.0",
|
||||
|
@ -100,6 +102,8 @@
|
|||
"next": "^14.2.3",
|
||||
"next-auth": "beta",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-hkdf": "^0.0.2",
|
||||
"node-jose": "^2.2.0",
|
||||
"nps": "^5.10.0",
|
||||
"objection": "^3.1.3",
|
||||
"passport": "^0.7.0",
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
"ajv": "^8.13.0",
|
||||
"app-store-server-api": "^0.7.0",
|
||||
"aws-sdk": "^2.1616.0",
|
||||
"base64url": "3.0.1",
|
||||
"buf": "0.1.1",
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"cldr-core": "^44.1.0",
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
"backbone": "^1.6.0",
|
||||
"backbone.cocktail": "0.5.15",
|
||||
"base32-decode": "1.0.0",
|
||||
"base64url": "3.0.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cache-loader": "^4.1.0",
|
||||
|
|
|
@ -62,6 +62,9 @@
|
|||
"@fxa/vendored/common-password-list": [
|
||||
"libs/vendored/common-password-list/src/index.ts"
|
||||
],
|
||||
"@fxa/vendored/crypto-relier": [
|
||||
"libs/vendored/crypto-relier/src/index.ts"
|
||||
],
|
||||
"@fxa/vendored/incremental-encoder": [
|
||||
"libs/vendored/incremental-encoder/src/index.ts"
|
||||
],
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
declare module 'node-hkdf';
|
|
@ -0,0 +1 @@
|
|||
declare module 'node-jose';
|
43
yarn.lock
43
yarn.lock
|
@ -27621,7 +27621,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64url@npm:3.0.1, base64url@npm:^3.0.0":
|
||||
"base64url@npm:^3.0.0, base64url@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "base64url@npm:3.0.1"
|
||||
checksum: a77b2a3a526b3343e25be424de3ae0aa937d78f6af7c813ef9020ef98001c0f4e2323afcd7d8b2d2978996bf8c42445c3e9f60c218c622593e5fdfd54a3d6e18
|
||||
|
@ -34009,6 +34009,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es6-promise@npm:^4.2.8":
|
||||
version: 4.2.8
|
||||
resolution: "es6-promise@npm:4.2.8"
|
||||
checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild-plugin-alias@npm:^0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "esbuild-plugin-alias@npm:0.2.1"
|
||||
|
@ -37843,7 +37850,6 @@ fsevents@~2.1.1:
|
|||
audit-filter: ^0.5.0
|
||||
aws-sdk: ^2.1616.0
|
||||
babel-loader: ^9.1.3
|
||||
base64url: 3.0.1
|
||||
binary-split: 1.0.5
|
||||
buf: 0.1.1
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
|
@ -37979,7 +37985,6 @@ fsevents@~2.1.1:
|
|||
backbone: ^1.6.0
|
||||
backbone.cocktail: 0.5.15
|
||||
base32-decode: 1.0.0
|
||||
base64url: 3.0.1
|
||||
body-parser: ^1.20.1
|
||||
buffer: ^6.0.3
|
||||
cache-loader: ^4.1.0
|
||||
|
@ -38850,6 +38855,7 @@ fsevents@~2.1.1:
|
|||
"@typescript-eslint/parser": ^7.1.1
|
||||
autoprefixer: ^10.4.14
|
||||
babel-jest: ^29.7.0
|
||||
base64url: ^3.0.1
|
||||
bn.js: ^5.2.1
|
||||
class-transformer: ^0.5.1
|
||||
class-validator: ^0.14.1
|
||||
|
@ -38877,6 +38883,7 @@ fsevents@~2.1.1:
|
|||
jest: ^29.5.0
|
||||
jest-environment-jsdom: ^29.5.0
|
||||
jest-environment-node: ^29.7.0
|
||||
jose: ^5.3.0
|
||||
json: ^11.0.0
|
||||
knex: ^3.1.0
|
||||
kysely: ^0.27.2
|
||||
|
@ -38891,6 +38898,8 @@ fsevents@~2.1.1:
|
|||
next: ^14.2.3
|
||||
next-auth: beta
|
||||
node-fetch: ^2.6.7
|
||||
node-hkdf: ^0.0.2
|
||||
node-jose: ^2.2.0
|
||||
nps: ^5.10.0
|
||||
nx: 18.3.1
|
||||
nx-cloud: 18.0.0
|
||||
|
@ -47949,7 +47958,7 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"long@npm:^5.2.1":
|
||||
"long@npm:^5.2.0, long@npm:^5.2.1":
|
||||
version: 5.2.3
|
||||
resolution: "long@npm:5.2.3"
|
||||
checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897
|
||||
|
@ -50890,7 +50899,7 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-hkdf@npm:0.0.2":
|
||||
"node-hkdf@npm:0.0.2, node-hkdf@npm:^0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "node-hkdf@npm:0.0.2"
|
||||
checksum: 20ce3a8877f96e8cd058cd8049caa24a559d69fb0c95d68449e25b36c954ee3018522e74ead9a22728061b0d3273dcd2b3b45bc7a8303943a64dc09a1b9ae807
|
||||
|
@ -50927,6 +50936,23 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-jose@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "node-jose@npm:2.2.0"
|
||||
dependencies:
|
||||
base64url: ^3.0.1
|
||||
buffer: ^6.0.3
|
||||
es6-promise: ^4.2.8
|
||||
lodash: ^4.17.21
|
||||
long: ^5.2.0
|
||||
node-forge: ^1.2.1
|
||||
pako: ^2.0.4
|
||||
process: ^0.11.10
|
||||
uuid: ^9.0.0
|
||||
checksum: ec021aaf12e256a88e0f8504b529d94685b9af774d3009048d4b005294add7735a99ef771f135a0cf6648c2c2c9c82ea4883f1c79b99fe6003b700e67819e24f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-libs-browser@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "node-libs-browser@npm:2.2.1"
|
||||
|
@ -52488,6 +52514,13 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pako@npm:^2.0.4":
|
||||
version: 2.1.0
|
||||
resolution: "pako@npm:2.1.0"
|
||||
checksum: 71666548644c9a4d056bcaba849ca6fd7242c6cf1af0646d3346f3079a1c7f4a66ffec6f7369ee0dc88f61926c10d6ab05da3e1fca44b83551839e89edd75a3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parallel-transform@npm:^1.1.0":
|
||||
version: 1.2.0
|
||||
resolution: "parallel-transform@npm:1.2.0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче