From 8771178ee38d790a8ea2c802faa31dd93a333763 Mon Sep 17 00:00:00 2001 From: Ben Bangert <100193+bbangert@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:40:15 -0700 Subject: [PATCH] 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 --- libs/vendored/crypto-relier/.eslintrc.json | 23 + libs/vendored/crypto-relier/README.md | 15 + libs/vendored/crypto-relier/jest.config.ts | 11 + libs/vendored/crypto-relier/package.json | 4 + libs/vendored/crypto-relier/project.json | 28 ++ libs/vendored/crypto-relier/src/index.ts | 6 + .../src/lib/deriver/deriver-utils.ts | 44 ++ .../src/lib/deriver/driver-utils.spec.ts | 103 ++++ .../crypto-relier/src/lib/deriver/index.ts | 8 + .../src/lib/deriver/scoped-keys.spec.ts | 448 ++++++++++++++++++ .../src/lib/deriver/scoped-keys.ts | 188 ++++++++ .../crypto-relier/src/lib/relier/index.ts | 5 + .../src/lib/relier/key-utils.spec.ts | 83 ++++ .../crypto-relier/src/lib/relier/key-utils.ts | 49 ++ libs/vendored/crypto-relier/tsconfig.json | 16 + libs/vendored/crypto-relier/tsconfig.lib.json | 11 + .../vendored/crypto-relier/tsconfig.spec.json | 14 + package.json | 4 + packages/fxa-auth-server/package.json | 1 - packages/fxa-content-server/package.json | 1 - tsconfig.base.json | 3 + types/node-hkdf/index.d.ts | 1 + types/node-jose/index.d.ts | 1 + yarn.lock | 43 +- 24 files changed, 1103 insertions(+), 7 deletions(-) create mode 100644 libs/vendored/crypto-relier/.eslintrc.json create mode 100644 libs/vendored/crypto-relier/README.md create mode 100644 libs/vendored/crypto-relier/jest.config.ts create mode 100644 libs/vendored/crypto-relier/package.json create mode 100644 libs/vendored/crypto-relier/project.json create mode 100644 libs/vendored/crypto-relier/src/index.ts create mode 100644 libs/vendored/crypto-relier/src/lib/deriver/deriver-utils.ts create mode 100644 libs/vendored/crypto-relier/src/lib/deriver/driver-utils.spec.ts create mode 100644 libs/vendored/crypto-relier/src/lib/deriver/index.ts create mode 100644 libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.spec.ts create mode 100644 libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.ts create mode 100644 libs/vendored/crypto-relier/src/lib/relier/index.ts create mode 100644 libs/vendored/crypto-relier/src/lib/relier/key-utils.spec.ts create mode 100644 libs/vendored/crypto-relier/src/lib/relier/key-utils.ts create mode 100644 libs/vendored/crypto-relier/tsconfig.json create mode 100644 libs/vendored/crypto-relier/tsconfig.lib.json create mode 100644 libs/vendored/crypto-relier/tsconfig.spec.json create mode 100644 types/node-hkdf/index.d.ts create mode 100644 types/node-jose/index.d.ts diff --git a/libs/vendored/crypto-relier/.eslintrc.json b/libs/vendored/crypto-relier/.eslintrc.json new file mode 100644 index 0000000000..e3f4dc691b --- /dev/null +++ b/libs/vendored/crypto-relier/.eslintrc.json @@ -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": {} + } + ] +} diff --git a/libs/vendored/crypto-relier/README.md b/libs/vendored/crypto-relier/README.md new file mode 100644 index 0000000000..87020230a2 --- /dev/null +++ b/libs/vendored/crypto-relier/README.md @@ -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). diff --git a/libs/vendored/crypto-relier/jest.config.ts b/libs/vendored/crypto-relier/jest.config.ts new file mode 100644 index 0000000000..2bb15a037e --- /dev/null +++ b/libs/vendored/crypto-relier/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'crypto-relier', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/vendored/crypto-relier', +}; diff --git a/libs/vendored/crypto-relier/package.json b/libs/vendored/crypto-relier/package.json new file mode 100644 index 0000000000..b85f50f167 --- /dev/null +++ b/libs/vendored/crypto-relier/package.json @@ -0,0 +1,4 @@ +{ + "name": "@fxa/vendored/crypto-relier", + "version": "0.0.1" +} diff --git a/libs/vendored/crypto-relier/project.json b/libs/vendored/crypto-relier/project.json new file mode 100644 index 0000000000..84fc4ecae0 --- /dev/null +++ b/libs/vendored/crypto-relier/project.json @@ -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$).*$"] + } + } + } +} diff --git a/libs/vendored/crypto-relier/src/index.ts b/libs/vendored/crypto-relier/src/index.ts new file mode 100644 index 0000000000..7bb1addd9f --- /dev/null +++ b/libs/vendored/crypto-relier/src/index.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'; diff --git a/libs/vendored/crypto-relier/src/lib/deriver/deriver-utils.ts b/libs/vendored/crypto-relier/src/lib/deriver/deriver-utils.ts new file mode 100644 index 0000000000..05ed9c5de6 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/deriver/deriver-utils.ts @@ -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); + } +} diff --git a/libs/vendored/crypto-relier/src/lib/deriver/driver-utils.spec.ts b/libs/vendored/crypto-relier/src/lib/deriver/driver-utils.spec.ts new file mode 100644 index 0000000000..23b1991651 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/deriver/driver-utils.spec.ts @@ -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'); + }); + }); +}); diff --git a/libs/vendored/crypto-relier/src/lib/deriver/index.ts b/libs/vendored/crypto-relier/src/lib/deriver/index.ts new file mode 100644 index 0000000000..7c0c3c846f --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/deriver/index.ts @@ -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'; diff --git a/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.spec.ts b/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.spec.ts new file mode 100644 index 0000000000..161fd40cf3 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.spec.ts @@ -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' + ); + }); + }); +}); diff --git a/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.ts b/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.ts new file mode 100644 index 0000000000..76785a3a87 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/deriver/scoped-keys.ts @@ -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 = { + 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 = { + 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 { + 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); + }); + }); + } +} diff --git a/libs/vendored/crypto-relier/src/lib/relier/index.ts b/libs/vendored/crypto-relier/src/lib/relier/index.ts new file mode 100644 index 0000000000..af8542cd76 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/relier/index.ts @@ -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'; diff --git a/libs/vendored/crypto-relier/src/lib/relier/key-utils.spec.ts b/libs/vendored/crypto-relier/src/lib/relier/key-utils.spec.ts new file mode 100644 index 0000000000..68b0849dd0 --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/relier/key-utils.spec.ts @@ -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: '', + k: '', + 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', + }, + }); + }); + }); +}); diff --git a/libs/vendored/crypto-relier/src/lib/relier/key-utils.ts b/libs/vendored/crypto-relier/src/lib/relier/key-utils.ts new file mode 100644 index 0000000000..6d63d00b7f --- /dev/null +++ b/libs/vendored/crypto-relier/src/lib/relier/key-utils.ts @@ -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)); + } +} diff --git a/libs/vendored/crypto-relier/tsconfig.json b/libs/vendored/crypto-relier/tsconfig.json new file mode 100644 index 0000000000..25f7201d87 --- /dev/null +++ b/libs/vendored/crypto-relier/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/vendored/crypto-relier/tsconfig.lib.json b/libs/vendored/crypto-relier/tsconfig.lib.json new file mode 100644 index 0000000000..e583571eac --- /dev/null +++ b/libs/vendored/crypto-relier/tsconfig.lib.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"] +} diff --git a/libs/vendored/crypto-relier/tsconfig.spec.json b/libs/vendored/crypto-relier/tsconfig.spec.json new file mode 100644 index 0000000000..69a251f328 --- /dev/null +++ b/libs/vendored/crypto-relier/tsconfig.spec.json @@ -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" + ] +} diff --git a/package.json b/package.json index 8e990c25b6..fb9dd2d2c0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/fxa-auth-server/package.json b/packages/fxa-auth-server/package.json index 1eef11c18e..1128de6f70 100644 --- a/packages/fxa-auth-server/package.json +++ b/packages/fxa-auth-server/package.json @@ -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", diff --git a/packages/fxa-content-server/package.json b/packages/fxa-content-server/package.json index 9735c92243..a90e9e8beb 100644 --- a/packages/fxa-content-server/package.json +++ b/packages/fxa-content-server/package.json @@ -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", diff --git a/tsconfig.base.json b/tsconfig.base.json index 83f2e5f30b..8591e3cb1c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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" ], diff --git a/types/node-hkdf/index.d.ts b/types/node-hkdf/index.d.ts new file mode 100644 index 0000000000..31233e003f --- /dev/null +++ b/types/node-hkdf/index.d.ts @@ -0,0 +1 @@ +declare module 'node-hkdf'; diff --git a/types/node-jose/index.d.ts b/types/node-jose/index.d.ts new file mode 100644 index 0000000000..723f7be77d --- /dev/null +++ b/types/node-jose/index.d.ts @@ -0,0 +1 @@ +declare module 'node-jose'; diff --git a/yarn.lock b/yarn.lock index 7040dfb596..b2c36ff317 100644 --- a/yarn.lock +++ b/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"