From 39ed9359feb24e74e5e5682e1724126e5a489a6d Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 12 Jan 2024 13:05:35 -0600 Subject: [PATCH] =?UTF-8?q?[AC-1743]=20pt.=202.5=20=E2=AE=95=20Fix=20the?= =?UTF-8?q?=20unit=20tests=20for=20good=20(#398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove uneeded redirects to a tsconfig.spec.json * Got the tests running again * Delete webCryptoFunctionService * Delete unused utils tests * Add a test workflow --- .github/workflows/test.yml | 54 ++ jest.config.js | 45 +- jslib/angular/jest.config.js | 16 - .../src/services/jslib-services.module.ts | 6 - jslib/common/custom-matchers.d.ts | 15 - jslib/common/jest.config.js | 17 - jslib/common/spec/index.ts | 1 - jslib/common/spec/matchers/index.ts | 1 - .../spec/matchers/to-equal-buffer.spec.ts | 25 - jslib/common/spec/matchers/to-equal-buffer.ts | 31 - jslib/common/spec/misc/utils.spec.ts | 73 --- .../webCryptoFunction.service.spec.ts | 558 ------------------ .../src/services/webCryptoFunction.service.ts | 356 ----------- jslib/common/test.setup.ts | 17 - jslib/electron/jest.config.js | 17 - jslib/electron/test.setup.ts | 0 jslib/node/jest.config.js | 17 - jslib/node/test.setup.ts | 0 jslib/shared/es2020-transformer.ts | 36 -- jslib/shared/jest.config.angular.js | 32 - jslib/shared/jest.config.ts.js | 29 - jslib/shared/test.environment.ts | 22 - jslib/shared/tsconfig.libs.json | 12 - package.json | 3 +- jslib/angular/test.setup.ts => test.setup.ts | 1 + tsconfig.eslint.json | 2 +- tsconfig.json | 4 +- 27 files changed, 90 insertions(+), 1300 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 jslib/angular/jest.config.js delete mode 100644 jslib/common/custom-matchers.d.ts delete mode 100644 jslib/common/jest.config.js delete mode 100644 jslib/common/spec/index.ts delete mode 100644 jslib/common/spec/matchers/index.ts delete mode 100644 jslib/common/spec/matchers/to-equal-buffer.spec.ts delete mode 100644 jslib/common/spec/matchers/to-equal-buffer.ts delete mode 100644 jslib/common/spec/misc/utils.spec.ts delete mode 100644 jslib/common/spec/web/services/webCryptoFunction.service.spec.ts delete mode 100644 jslib/common/src/services/webCryptoFunction.service.ts delete mode 100644 jslib/common/test.setup.ts delete mode 100644 jslib/electron/jest.config.js delete mode 100644 jslib/electron/test.setup.ts delete mode 100644 jslib/node/jest.config.js delete mode 100644 jslib/node/test.setup.ts delete mode 100644 jslib/shared/es2020-transformer.ts delete mode 100644 jslib/shared/jest.config.angular.js delete mode 100644 jslib/shared/jest.config.ts.js delete mode 100644 jslib/shared/test.environment.ts delete mode 100644 jslib/shared/tsconfig.libs.json rename jslib/angular/test.setup.ts => test.setup.ts (99%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f91c396e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +--- +name: Run tests + +on: + workflow_dispatch: + push: + branches: + - "main" + - "rc" + - "hotfix-rc-*" + pull_request: {} + +defaults: + run: + shell: bash + +jobs: + test: + name: Run tests + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Get Node Version + id: retrieve-node-version + run: | + NODE_NVMRC=$(cat .nvmrc) + NODE_VERSION=${NODE_NVMRC/v/''} + echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + + - name: Set up Node + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ steps.retrieve-node-version.outputs.node_version }} + + - name: Print environment + run: | + node --version + npm --version + + - name: Install Node dependencies + run: npm ci + + # We use isolatedModules: true which disables typechecking in tests + # Tests in apps/ are typechecked when their app is built, so we just do it here for libs/ + # See https://bitwarden.atlassian.net/browse/EC-497 + - name: Run typechecking + run: npm run test:types --coverage + + - name: Run tests + run: npm run test diff --git a/jest.config.js b/jest.config.js index 9f4ae1a2..5aad2b0a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,27 +1,42 @@ const { pathsToModuleNameMapper } = require("ts-jest"); - const { compilerOptions } = require("./tsconfig"); -/** @type {import('jest').Config} */ +const tsPreset = require("ts-jest/jest-preset"); +const angularPreset = require("jest-preset-angular/jest-preset"); +const { defaultTransformerOptions } = require("jest-preset-angular/presets"); + +/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - reporters: ["default", "jest-junit"], + // ...tsPreset, + // ...angularPreset, + preset: "jest-preset-angular", - collectCoverage: true, - coverageReporters: ["html", "lcov"], - coverageDirectory: "coverage", + testEnvironment: "jsdom", + testMatch: ["**/+(*.)+(spec).+(ts)"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), - projects: [ - "/jslib/angular/jest.config.js", - "/jslib/common/jest.config.js", - "/jslib/electron/jest.config.js", - "/jslib/node/jest.config.js", - ], + roots: [""], + modulePaths: [compilerOptions.baseUrl], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "/" }), + setupFilesAfterEnv: ["/test.setup.ts"], // Workaround for a memory leak that crashes tests in CI: // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 // Also anecdotally improves performance when run locally maxWorkers: 3, + + transform: { + "^.+\\.tsx?$": [ + "jest-preset-angular", + // 'ts-jest', + { + ...defaultTransformerOptions, + tsconfig: "./tsconfig.json", + // Further workaround for memory leak, recommended here: + // https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014 + // Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code + // See https://bitwarden.atlassian.net/browse/EC-497 for more info + isolatedModules: true, + }, + ], + }, }; diff --git a/jslib/angular/jest.config.js b/jslib/angular/jest.config.js deleted file mode 100644 index 25a1d4f8..00000000 --- a/jslib/angular/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -const { pathsToModuleNameMapper } = require("ts-jest"); - -const { compilerOptions } = require("../shared/tsconfig.libs"); - -const sharedConfig = require("../shared/jest.config.angular"); - -/** @type {import('jest').Config} */ -module.exports = { - ...sharedConfig, - displayName: "libs/angular tests", - preset: "jest-preset-angular", - setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), -}; diff --git a/jslib/angular/src/services/jslib-services.module.ts b/jslib/angular/src/services/jslib-services.module.ts index 9333d016..99901d72 100644 --- a/jslib/angular/src/services/jslib-services.module.ts +++ b/jslib/angular/src/services/jslib-services.module.ts @@ -72,7 +72,6 @@ import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service" import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service"; import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service"; import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.service"; -import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoFunction.service"; import { AuthGuardService } from "./auth-guard.service"; import { BroadcasterService } from "./broadcaster.service"; @@ -420,11 +419,6 @@ import { ValidationService } from "./validation.service"; StateServiceAbstraction, ], }, - { - provide: CryptoFunctionServiceAbstraction, - useClass: WebCryptoFunctionService, - deps: ["WINDOW"], - }, { provide: EventServiceAbstraction, useClass: EventService, diff --git a/jslib/common/custom-matchers.d.ts b/jslib/common/custom-matchers.d.ts deleted file mode 100644 index 214529ff..00000000 --- a/jslib/common/custom-matchers.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CustomMatchers } from "./test.setup"; - -// This declares the types for our custom matchers so that they're recognised by Typescript -// This file must also be included in the TS compilation (via the tsconfig.json "include" property) to be recognised by -// vscode - -/* eslint-disable */ -declare global { - namespace jest { - interface Expect extends CustomMatchers {} - interface Matchers extends CustomMatchers {} - interface InverseAsymmetricMatchers extends CustomMatchers {} - } -} -/* eslint-enable */ diff --git a/jslib/common/jest.config.js b/jslib/common/jest.config.js deleted file mode 100644 index d7f78abb..00000000 --- a/jslib/common/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const { pathsToModuleNameMapper } = require("ts-jest"); - -const { compilerOptions } = require("../shared/tsconfig.libs"); - -const sharedConfig = require("../shared/jest.config.ts"); - -/** @type {import('jest').Config} */ -module.exports = { - ...sharedConfig, - displayName: "libs/common tests", - preset: "ts-jest", - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), -}; diff --git a/jslib/common/spec/index.ts b/jslib/common/spec/index.ts deleted file mode 100644 index 514a6403..00000000 --- a/jslib/common/spec/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./matchers"; diff --git a/jslib/common/spec/matchers/index.ts b/jslib/common/spec/matchers/index.ts deleted file mode 100644 index 59f6409f..00000000 --- a/jslib/common/spec/matchers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./to-equal-buffer"; diff --git a/jslib/common/spec/matchers/to-equal-buffer.spec.ts b/jslib/common/spec/matchers/to-equal-buffer.spec.ts deleted file mode 100644 index ccf57423..00000000 --- a/jslib/common/spec/matchers/to-equal-buffer.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { makeStaticByteArray } from "../utils"; - -describe("toEqualBuffer custom matcher", () => { - it("matches identical ArrayBuffers", () => { - const array = makeStaticByteArray(10); - expect(array.buffer).toEqualBuffer(array.buffer); - }); - - it("matches an identical ArrayBuffer and Uint8Array", () => { - const array = makeStaticByteArray(10); - expect(array.buffer).toEqualBuffer(array); - }); - - it("doesn't match different ArrayBuffers", () => { - const array1 = makeStaticByteArray(10); - const array2 = makeStaticByteArray(10, 11); - expect(array1.buffer).not.toEqualBuffer(array2.buffer); - }); - - it("doesn't match a different ArrayBuffer and Uint8Array", () => { - const array1 = makeStaticByteArray(10); - const array2 = makeStaticByteArray(10, 11); - expect(array1.buffer).not.toEqualBuffer(array2); - }); -}); diff --git a/jslib/common/spec/matchers/to-equal-buffer.ts b/jslib/common/spec/matchers/to-equal-buffer.ts deleted file mode 100644 index e723d85a..00000000 --- a/jslib/common/spec/matchers/to-equal-buffer.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * The inbuilt toEqual() matcher will always return TRUE when provided with 2 ArrayBuffers. - * This is because an ArrayBuffer must be wrapped in a new Uint8Array to be accessible. - * This custom matcher will automatically instantiate a new Uint8Array on the received value - * (and optionally, the expected value) and then call toEqual() on the resulting Uint8Arrays. - */ -export const toEqualBuffer: jest.CustomMatcher = function ( - received: ArrayBuffer | Uint8Array, - expected: ArrayBuffer | Uint8Array -) { - received = new Uint8Array(received); - expected = new Uint8Array(expected); - - if (this.equals(received, expected)) { - return { - message: () => `expected -${received} -not to match -${expected}`, - pass: true, - }; - } - - return { - message: () => `expected -${received} -to match -${expected}`, - pass: false, - }; -}; diff --git a/jslib/common/spec/misc/utils.spec.ts b/jslib/common/spec/misc/utils.spec.ts deleted file mode 100644 index cada0117..00000000 --- a/jslib/common/spec/misc/utils.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Utils } from "@/jslib/common/src/misc/utils"; - -describe("Utils Service", () => { - describe("getDomain", () => { - it("should fail for invalid urls", () => { - expect(Utils.getDomain(null)).toBeNull(); - expect(Utils.getDomain(undefined)).toBeNull(); - expect(Utils.getDomain(" ")).toBeNull(); - expect(Utils.getDomain('https://bit!:"_&ward.com')).toBeNull(); - expect(Utils.getDomain("bitwarden")).toBeNull(); - }); - - it("should fail for data urls", () => { - expect(Utils.getDomain("data:image/jpeg;base64,AAA")).toBeNull(); - }); - - it("should handle urls without protocol", () => { - expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getDomain("wrong://bitwarden.com")).toBe("bitwarden.com"); - }); - - it("should handle valid urls", () => { - expect(Utils.getDomain("https://bitwarden")).toBe("bitwarden"); - expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getDomain("http://bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getDomain("http://vault.bitwarden.com")).toBe("bitwarden.com"); - expect( - Utils.getDomain("https://user:password@bitwarden.com:8080/password/sites?and&query#hash") - ).toBe("bitwarden.com"); - expect(Utils.getDomain("https://bitwarden.unknown")).toBe("bitwarden.unknown"); - }); - - it("should support localhost and IP", () => { - expect(Utils.getDomain("https://localhost")).toBe("localhost"); - expect(Utils.getDomain("https://192.168.1.1")).toBe("192.168.1.1"); - }); - - it("should reject invalid hostnames", () => { - expect(Utils.getDomain("https://mywebsite.com$.mywebsite.com")).toBeNull(); - expect(Utils.getDomain("https://mywebsite.com!.mywebsite.com")).toBeNull(); - }); - }); - - describe("getHostname", () => { - it("should fail for invalid urls", () => { - expect(Utils.getHostname(null)).toBeNull(); - expect(Utils.getHostname(undefined)).toBeNull(); - expect(Utils.getHostname(" ")).toBeNull(); - expect(Utils.getHostname('https://bit!:"_&ward.com')).toBeNull(); - expect(Utils.getHostname("bitwarden")).toBeNull(); - }); - - it("should handle valid urls", () => { - expect(Utils.getHostname("bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getHostname("http://bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getHostname("http://vault.bitwarden.com")).toBe("vault.bitwarden.com"); - }); - - it("should support localhost and IP", () => { - expect(Utils.getHostname("https://localhost")).toBe("localhost"); - expect(Utils.getHostname("https://192.168.1.1")).toBe("192.168.1.1"); - }); - }); - - describe("newGuid", () => { - it("should create a valid guid", () => { - const validGuid = - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - expect(Utils.newGuid()).toMatch(validGuid); - }); - }); -}); diff --git a/jslib/common/spec/web/services/webCryptoFunction.service.spec.ts b/jslib/common/spec/web/services/webCryptoFunction.service.spec.ts deleted file mode 100644 index 06a188df..00000000 --- a/jslib/common/spec/web/services/webCryptoFunction.service.spec.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { Substitute } from "@fluffy-spoon/substitute"; - -import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; -import { Utils } from "@/jslib/common/src/misc/utils"; -import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey"; -import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoFunction.service"; - -const RsaPublicKey = - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + - "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + - "RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" + - "084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" + - "xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB"; -const RsaPrivateKey = - "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" + - "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" + - "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" + - "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" + - "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" + - "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" + - "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" + - "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" + - "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" + - "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" + - "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" + - "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" + - "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" + - "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" + - "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" + - "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" + - "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" + - "BokBGnjFnTnKcs7nv/O8="; - -const Sha1Mac = "4d4c223f95dc577b665ec4ccbcb680b80a397038"; -const Sha256Mac = "6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f"; -const Sha512Mac = - "21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c" + - "5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca"; - -describe("WebCrypto Function Service", () => { - describe("pbkdf2", () => { - const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I="; - const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I="; - const unicode256Key = "ZdeOata6xoRpB4DLp8zHhXz5kLmkWtX5pd+TdRH8w8w="; - - const regular512Key = - "liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/JFHFKQW3ZkI81qVlwTiCpb+cFXzs+57" + - "eyhhx5wfKo5Cg=="; - const utf8512Key = - "df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANu5T+scq55hCcE4dG4T/MhAk2exw8j7ixRN" + - "zXANiVZpnw=="; - const unicode512Key = - "FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3XtxtnpdqqpblX6JjuMZt/dYYNMOrb2+mD" + - "L3FiQDTROh1lg=="; - - testPbkdf2("sha256", regular256Key, utf8256Key, unicode256Key); - testPbkdf2("sha512", regular512Key, utf8512Key, unicode512Key); - }); - - describe("hkdf", () => { - const regular256Key = "qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw="; - const utf8256Key = "6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU="; - const unicode256Key = "gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A="; - - const regular512Key = "xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM="; - const utf8512Key = "XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY="; - const unicode512Key = "148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc="; - - testHkdf("sha256", regular256Key, utf8256Key, unicode256Key); - testHkdf("sha512", regular512Key, utf8512Key, unicode512Key); - }); - - describe("hkdfExpand", () => { - const prk16Byte = "criAmKtfzxanbgea5/kelQ=="; - const prk32Byte = "F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y="; - const prk64Byte = - "ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+" + - "gUJ28p8y+hFh3EI9pcrEWaNvFYonQ=="; - - testHkdfExpand("sha256", prk32Byte, 32, "BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8="); - testHkdfExpand( - "sha256", - prk32Byte, - 64, - "BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+" + - "/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA==" - ); - testHkdfExpand("sha512", prk64Byte, 32, "uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk="); - testHkdfExpand( - "sha512", - prk64Byte, - 64, - "uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+" + - "MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w==" - ); - - it("should fail with prk too small", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const f = cryptoFunctionService.hkdfExpand( - Utils.fromB64ToArray(prk16Byte), - "info", - 32, - "sha256" - ); - await expect(f).rejects.toEqual(new Error("prk is too small.")); - }); - - it("should fail with outputByteSize is too large", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const f = cryptoFunctionService.hkdfExpand( - Utils.fromB64ToArray(prk32Byte), - "info", - 8161, - "sha256" - ); - await expect(f).rejects.toEqual(new Error("outputByteSize is too large.")); - }); - }); - - describe("hash", () => { - const regular1Hash = "2a241604fb921fad12bf877282457268e1dccb70"; - const utf81Hash = "85672798dc5831e96d6c48655d3d39365a9c88b6"; - const unicode1Hash = "39c975935054a3efc805a9709b60763a823a6ad4"; - - const regular256Hash = "2b8e96031d352a8655d733d7a930b5ffbea69dc25cf65c7bca7dd946278908b2"; - const utf8256Hash = "25fe8440f5b01ed113b0a0e38e721b126d2f3f77a67518c4a04fcde4e33eeb9d"; - const unicode256Hash = "adc1c0c2afd6e92cefdf703f9b6eb2c38e0d6d1a040c83f8505c561fea58852e"; - - const regular512Hash = - "c15cf11d43bde333647e3f559ec4193bb2edeaa0e8b902772f514cdf3f785a3f49a6e02a4b87b3" + - "b47523271ad45b7e0aebb5cdcc1bc54815d256eb5dcb80da9d"; - const utf8512Hash = - "035c31a877a291af09ed2d3a1a293e69c3e079ea2cecc00211f35e6bce10474ca3ad6e30b59e26118" + - "37463f20969c5bc95282965a051a88f8cdf2e166549fcdd"; - const unicode512Hash = - "2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d" + - "9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae"; - - const regularMd5 = "5eceffa53a5fd58c44134211e2c5f522"; - const utf8Md5 = "3abc9433c09551b939c80aa0aa3174e1"; - const unicodeMd5 = "85ae134072c8d81257933f7045ba17ca"; - - testHash("sha1", regular1Hash, utf81Hash, unicode1Hash); - testHash("sha256", regular256Hash, utf8256Hash, unicode256Hash); - testHash("sha512", regular512Hash, utf8512Hash, unicode512Hash); - testHash("md5", regularMd5, utf8Md5, unicodeMd5); - }); - - describe("hmac", () => { - testHmac("sha1", Sha1Mac); - testHmac("sha256", Sha256Mac); - testHmac("sha512", Sha512Mac); - }); - - describe("compare", () => { - it("should successfully compare two of the same values", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const equal = await cryptoFunctionService.compare(a.buffer, a.buffer); - expect(equal).toBe(true); - }); - - it("should successfully compare two different values of the same length", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const b = new Uint8Array(2); - b[0] = 3; - b[1] = 4; - const equal = await cryptoFunctionService.compare(a.buffer, b.buffer); - expect(equal).toBe(false); - }); - - it("should successfully compare two different values of different lengths", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const b = new Uint8Array(2); - b[0] = 3; - const equal = await cryptoFunctionService.compare(a.buffer, b.buffer); - expect(equal).toBe(false); - }); - }); - - describe("hmacFast", () => { - testHmacFast("sha1", Sha1Mac); - testHmacFast("sha256", Sha256Mac); - testHmacFast("sha512", Sha512Mac); - }); - - describe("compareFast", () => { - it("should successfully compare two of the same values", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const aByteString = Utils.fromBufferToByteString(a.buffer); - const equal = await cryptoFunctionService.compareFast(aByteString, aByteString); - expect(equal).toBe(true); - }); - - it("should successfully compare two different values of the same length", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const aByteString = Utils.fromBufferToByteString(a.buffer); - const b = new Uint8Array(2); - b[0] = 3; - b[1] = 4; - const bByteString = Utils.fromBufferToByteString(b.buffer); - const equal = await cryptoFunctionService.compareFast(aByteString, bByteString); - expect(equal).toBe(false); - }); - - it("should successfully compare two different values of different lengths", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const a = new Uint8Array(2); - a[0] = 1; - a[1] = 2; - const aByteString = Utils.fromBufferToByteString(a.buffer); - const b = new Uint8Array(2); - b[0] = 3; - const bByteString = Utils.fromBufferToByteString(b.buffer); - const equal = await cryptoFunctionService.compareFast(aByteString, bByteString); - expect(equal).toBe(false); - }); - }); - - describe("aesEncrypt", () => { - it("should successfully encrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const data = Utils.fromUtf8ToArray("EncryptMe!"); - const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA=="); - }); - - it("should successfully encrypt and then decrypt data fast", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const value = "EncryptMe!"; - const data = Utils.fromUtf8ToArray(value); - const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const encData = Utils.fromBufferToB64(encValue); - const b64Iv = Utils.fromBufferToB64(iv.buffer); - const symKey = new SymmetricCryptoKey(key.buffer); - const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params); - expect(decValue).toBe(value); - }); - - it("should successfully encrypt and then decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const value = "EncryptMe!"; - const data = Utils.fromUtf8ToArray(value); - const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer); - expect(Utils.fromBufferToUtf8(decValue)).toBe(value); - }); - }); - - describe("aesDecryptFast", () => { - it("should successfully decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer); - const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer); - const data = "ByUF8vhyX4ddU9gcooznwA=="; - const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params); - expect(decValue).toBe("EncryptMe!"); - }); - }); - - describe("aesDecrypt", () => { - it("should successfully decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); - const decValue = await cryptoFunctionService.aesDecrypt(data.buffer, iv.buffer, key.buffer); - expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); - }); - }); - - describe("rsaEncrypt", () => { - it("should successfully encrypt and then decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const pubKey = Utils.fromB64ToArray(RsaPublicKey); - const privKey = Utils.fromB64ToArray(RsaPrivateKey); - const value = "EncryptMe!"; - const data = Utils.fromUtf8ToArray(value); - const encValue = await cryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, "sha1"); - const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, "sha1"); - expect(Utils.fromBufferToUtf8(decValue)).toBe(value); - }); - }); - - describe("rsaDecrypt", () => { - it("should successfully decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const privKey = Utils.fromB64ToArray(RsaPrivateKey); - const data = Utils.fromB64ToArray( - "A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV" + - "4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT" + - "zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D" + - "/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw==" - ); - const decValue = await cryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, "sha1"); - expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); - }); - }); - - describe("rsaExtractPublicKey", () => { - it("should successfully extract key", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const privKey = Utils.fromB64ToArray(RsaPrivateKey); - const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey.buffer); - expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey); - }); - }); - - describe("rsaGenerateKeyPair", () => { - testRsaGenerateKeyPair(1024); - testRsaGenerateKeyPair(2048); - - // Generating 4096 bit keys can be slow. Commenting it out to save CI. - // testRsaGenerateKeyPair(4096); - }); - - describe("randomBytes", () => { - it("should make a value of the correct length", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const randomData = await cryptoFunctionService.randomBytes(16); - expect(randomData.byteLength).toBe(16); - }); - - it("should not make the same value twice", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const randomData = await cryptoFunctionService.randomBytes(16); - const randomData2 = await cryptoFunctionService.randomBytes(16); - expect( - randomData.byteLength === randomData2.byteLength && randomData !== randomData2 - ).toBeTruthy(); - }); - }); -}); - -function testPbkdf2( - algorithm: "sha256" | "sha512", - regularKey: string, - utf8Key: string, - unicodeKey: string -) { - const regularEmail = "user@example.com"; - const utf8Email = "üser@example.com"; - - const regularPassword = "password"; - const utf8Password = "pǻssword"; - const unicodePassword = "😀password🙏"; - - it("should create valid " + algorithm + " key from regular input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); - expect(Utils.fromBufferToB64(key)).toBe(regularKey); - }); - - it("should create valid " + algorithm + " key from utf8 input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); - expect(Utils.fromBufferToB64(key)).toBe(utf8Key); - }); - - it("should create valid " + algorithm + " key from unicode input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); - expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); - }); - - it("should create valid " + algorithm + " key from array buffer input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.pbkdf2( - Utils.fromUtf8ToArray(regularPassword).buffer, - Utils.fromUtf8ToArray(regularEmail).buffer, - algorithm, - 5000 - ); - expect(Utils.fromBufferToB64(key)).toBe(regularKey); - }); -} - -function testHkdf( - algorithm: "sha256" | "sha512", - regularKey: string, - utf8Key: string, - unicodeKey: string -) { - const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ=="); - - const regularSalt = "salt"; - const utf8Salt = "üser_salt"; - const unicodeSalt = "😀salt🙏"; - - const regularInfo = "info"; - const utf8Info = "üser_info"; - const unicodeInfo = "😀info🙏"; - - it("should create valid " + algorithm + " key from regular input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm); - expect(Utils.fromBufferToB64(key)).toBe(regularKey); - }); - - it("should create valid " + algorithm + " key from utf8 input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm); - expect(Utils.fromBufferToB64(key)).toBe(utf8Key); - }); - - it("should create valid " + algorithm + " key from unicode input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm); - expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); - }); - - it("should create valid " + algorithm + " key from array buffer input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const key = await cryptoFunctionService.hkdf( - ikm, - Utils.fromUtf8ToArray(regularSalt).buffer, - Utils.fromUtf8ToArray(regularInfo).buffer, - 32, - algorithm - ); - expect(Utils.fromBufferToB64(key)).toBe(regularKey); - }); -} - -function testHkdfExpand( - algorithm: "sha256" | "sha512", - b64prk: string, - outputByteSize: number, - b64ExpectedOkm: string -) { - const info = "info"; - - it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const okm = await cryptoFunctionService.hkdfExpand( - Utils.fromB64ToArray(b64prk), - info, - outputByteSize, - algorithm - ); - expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm); - }); -} - -function testHash( - algorithm: "sha1" | "sha256" | "sha512" | "md5", - regularHash: string, - utf8Hash: string, - unicodeHash: string -) { - const regularValue = "HashMe!!"; - const utf8Value = "HǻshMe!!"; - const unicodeValue = "😀HashMe!!!🙏"; - - it("should create valid " + algorithm + " hash from regular input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const hash = await cryptoFunctionService.hash(regularValue, algorithm); - expect(Utils.fromBufferToHex(hash)).toBe(regularHash); - }); - - it("should create valid " + algorithm + " hash from utf8 input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const hash = await cryptoFunctionService.hash(utf8Value, algorithm); - expect(Utils.fromBufferToHex(hash)).toBe(utf8Hash); - }); - - it("should create valid " + algorithm + " hash from unicode input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const hash = await cryptoFunctionService.hash(unicodeValue, algorithm); - expect(Utils.fromBufferToHex(hash)).toBe(unicodeHash); - }); - - it("should create valid " + algorithm + " hash from array buffer input", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const hash = await cryptoFunctionService.hash( - Utils.fromUtf8ToArray(regularValue).buffer, - algorithm - ); - expect(Utils.fromBufferToHex(hash)).toBe(regularHash); - }); -} - -function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) { - it("should create valid " + algorithm + " hmac", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const computedMac = await cryptoFunctionService.hmac( - Utils.fromUtf8ToArray("SignMe!!").buffer, - Utils.fromUtf8ToArray("secretkey").buffer, - algorithm - ); - expect(Utils.fromBufferToHex(computedMac)).toBe(mac); - }); -} - -function testHmacFast(algorithm: "sha1" | "sha256" | "sha512", mac: string) { - it("should create valid " + algorithm + " hmac", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey").buffer); - const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!").buffer); - const computedMac = await cryptoFunctionService.hmacFast( - dataByteString, - keyByteString, - algorithm - ); - expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac).buffer)).toBe(mac); - }); -} - -function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { - it( - "should successfully generate a " + length + " bit key pair", - async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length); - expect(keyPair[0] == null || keyPair[1] == null).toBe(false); - const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); - expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); - }, - 30000 - ); -} - -function getWebCryptoFunctionService() { - const platformUtilsMock = Substitute.for(); - platformUtilsMock.isEdge().mimicks(() => navigator.userAgent.indexOf(" Edg/") !== -1); - - return new WebCryptoFunctionService(window); -} - -function makeStaticByteArray(length: number) { - const arr = new Uint8Array(length); - for (let i = 0; i < length; i++) { - arr[i] = i; - } - return arr; -} diff --git a/jslib/common/src/services/webCryptoFunction.service.ts b/jslib/common/src/services/webCryptoFunction.service.ts deleted file mode 100644 index b863f226..00000000 --- a/jslib/common/src/services/webCryptoFunction.service.ts +++ /dev/null @@ -1,356 +0,0 @@ -import * as forge from "node-forge"; - -import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; -import { Utils } from "../misc/utils"; -import { DecryptParameters } from "../models/domain/decryptParameters"; -import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; - -export class WebCryptoFunctionService implements CryptoFunctionService { - private crypto: Crypto; - private subtle: SubtleCrypto; - - constructor(win: Window) { - this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null; - this.subtle = - !!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null; - } - - async pbkdf2( - password: string | ArrayBuffer, - salt: string | ArrayBuffer, - algorithm: "sha256" | "sha512", - iterations: number - ): Promise { - const wcLen = algorithm === "sha256" ? 256 : 512; - const passwordBuf = this.toBuf(password); - const saltBuf = this.toBuf(salt); - - const pbkdf2Params: Pbkdf2Params = { - name: "PBKDF2", - salt: saltBuf, - iterations: iterations, - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - - const impKey = await this.subtle.importKey( - "raw", - passwordBuf, - { name: "PBKDF2" } as any, - false, - ["deriveBits"] - ); - return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); - } - - async hkdf( - ikm: ArrayBuffer, - salt: string | ArrayBuffer, - info: string | ArrayBuffer, - outputByteSize: number, - algorithm: "sha256" | "sha512" - ): Promise { - const saltBuf = this.toBuf(salt); - const infoBuf = this.toBuf(info); - - const hkdfParams: HkdfParams = { - name: "HKDF", - salt: saltBuf, - info: infoBuf, - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - - const impKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" } as any, false, [ - "deriveBits", - ]); - return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8); - } - - // ref: https://tools.ietf.org/html/rfc5869 - async hkdfExpand( - prk: ArrayBuffer, - info: string | ArrayBuffer, - outputByteSize: number, - algorithm: "sha256" | "sha512" - ): Promise { - const hashLen = algorithm === "sha256" ? 32 : 64; - if (outputByteSize > 255 * hashLen) { - throw new Error("outputByteSize is too large."); - } - const prkArr = new Uint8Array(prk); - if (prkArr.length < hashLen) { - throw new Error("prk is too small."); - } - const infoBuf = this.toBuf(info); - const infoArr = new Uint8Array(infoBuf); - let runningOkmLength = 0; - let previousT = new Uint8Array(0); - const n = Math.ceil(outputByteSize / hashLen); - const okm = new Uint8Array(n * hashLen); - for (let i = 0; i < n; i++) { - const t = new Uint8Array(previousT.length + infoArr.length + 1); - t.set(previousT); - t.set(infoArr, previousT.length); - t.set([i + 1], t.length - 1); - previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm)); - okm.set(previousT, runningOkmLength); - runningOkmLength += previousT.length; - if (runningOkmLength >= outputByteSize) { - break; - } - } - return okm.slice(0, outputByteSize).buffer; - } - - async hash( - value: string | ArrayBuffer, - algorithm: "sha1" | "sha256" | "sha512" | "md5" - ): Promise { - if (algorithm === "md5") { - const md = algorithm === "md5" ? forge.md.md5.create() : forge.md.sha1.create(); - const valueBytes = this.toByteString(value); - md.update(valueBytes, "raw"); - return Utils.fromByteStringToArray(md.digest().data).buffer; - } - - const valueBuf = this.toBuf(value); - return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf); - } - - async hmac( - value: ArrayBuffer, - key: ArrayBuffer, - algorithm: "sha1" | "sha256" | "sha512" - ): Promise { - const signingAlgorithm = { - name: "HMAC", - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - - const impKey = await this.subtle.importKey("raw", key, signingAlgorithm, false, ["sign"]); - return await this.subtle.sign(signingAlgorithm, impKey, value); - } - - // Safely compare two values in a way that protects against timing attacks (Double HMAC Verification). - // ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ - // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy - async compare(a: ArrayBuffer, b: ArrayBuffer): Promise { - const macKey = await this.randomBytes(32); - const signingAlgorithm = { - name: "HMAC", - hash: { name: "SHA-256" }, - }; - const impKey = await this.subtle.importKey("raw", macKey, signingAlgorithm, false, ["sign"]); - const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a); - const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b); - - if (mac1.byteLength !== mac2.byteLength) { - return false; - } - - const arr1 = new Uint8Array(mac1); - const arr2 = new Uint8Array(mac2); - for (let i = 0; i < arr2.length; i++) { - if (arr1[i] !== arr2[i]) { - return false; - } - } - - return true; - } - - hmacFast(value: string, key: string, algorithm: "sha1" | "sha256" | "sha512"): Promise { - const hmac = forge.hmac.create(); - hmac.start(algorithm, key); - hmac.update(value); - const bytes = hmac.digest().getBytes(); - return Promise.resolve(bytes); - } - - async compareFast(a: string, b: string): Promise { - const rand = await this.randomBytes(32); - const bytes = new Uint32Array(rand); - const buffer = forge.util.createBuffer(); - for (let i = 0; i < bytes.length; i++) { - buffer.putInt32(bytes[i]); - } - const macKey = buffer.getBytes(); - - const hmac = forge.hmac.create(); - hmac.start("sha256", macKey); - hmac.update(a); - const mac1 = hmac.digest().getBytes(); - - hmac.start(null, null); - hmac.update(b); - const mac2 = hmac.digest().getBytes(); - - const equals = mac1 === mac2; - return equals; - } - - async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ - "encrypt", - ]); - return await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data); - } - - aesDecryptFastParameters( - data: string, - iv: string, - mac: string, - key: SymmetricCryptoKey - ): DecryptParameters { - const p = new DecryptParameters(); - if (key.meta != null) { - p.encKey = key.meta.encKeyByteString; - p.macKey = key.meta.macKeyByteString; - } - - if (p.encKey == null) { - p.encKey = forge.util.decode64(key.encKeyB64); - } - p.data = forge.util.decode64(data); - p.iv = forge.util.decode64(iv); - p.macData = p.iv + p.data; - if (p.macKey == null && key.macKeyB64 != null) { - p.macKey = forge.util.decode64(key.macKeyB64); - } - if (mac != null) { - p.mac = forge.util.decode64(mac); - } - - // cache byte string keys for later - if (key.meta == null) { - key.meta = {}; - } - if (key.meta.encKeyByteString == null) { - key.meta.encKeyByteString = p.encKey; - } - if (p.macKey != null && key.meta.macKeyByteString == null) { - key.meta.macKeyByteString = p.macKey; - } - - return p; - } - - aesDecryptFast(parameters: DecryptParameters): Promise { - const dataBuffer = forge.util.createBuffer(parameters.data); - const decipher = forge.cipher.createDecipher("AES-CBC", parameters.encKey); - decipher.start({ iv: parameters.iv }); - decipher.update(dataBuffer); - decipher.finish(); - const val = decipher.output.toString(); - return Promise.resolve(val); - } - - async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ - "decrypt", - ]); - return await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data); - } - - async rsaEncrypt( - data: ArrayBuffer, - publicKey: ArrayBuffer, - algorithm: "sha1" | "sha256" - ): Promise { - // Note: Edge browser requires that we specify name and hash for both key import and decrypt. - // We cannot use the proper types here. - const rsaParams = { - name: "RSA-OAEP", - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]); - return await this.subtle.encrypt(rsaParams, impKey, data); - } - - async rsaDecrypt( - data: ArrayBuffer, - privateKey: ArrayBuffer, - algorithm: "sha1" | "sha256" - ): Promise { - // Note: Edge browser requires that we specify name and hash for both key import and decrypt. - // We cannot use the proper types here. - const rsaParams = { - name: "RSA-OAEP", - hash: { name: this.toWebCryptoAlgorithm(algorithm) }, - }; - const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]); - return await this.subtle.decrypt(rsaParams, impKey, data); - } - - async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { - const rsaParams = { - name: "RSA-OAEP", - // Have to specify some algorithm - hash: { name: this.toWebCryptoAlgorithm("sha1") }, - }; - const impPrivateKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, true, [ - "decrypt", - ]); - const jwkPrivateKey = await this.subtle.exportKey("jwk", impPrivateKey); - const jwkPublicKeyParams = { - kty: "RSA", - e: jwkPrivateKey.e, - n: jwkPrivateKey.n, - alg: "RSA-OAEP", - ext: true, - }; - const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [ - "encrypt", - ]); - return await this.subtle.exportKey("spki", impPublicKey); - } - - async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { - const rsaParams = { - name: "RSA-OAEP", - modulusLength: length, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 - // Have to specify some algorithm - hash: { name: this.toWebCryptoAlgorithm("sha1") }, - }; - const keyPair = (await this.subtle.generateKey(rsaParams, true, [ - "encrypt", - "decrypt", - ])) as CryptoKeyPair; - const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey); - const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey); - return [publicKey, privateKey]; - } - - randomBytes(length: number): Promise { - const arr = new Uint8Array(length); - this.crypto.getRandomValues(arr); - return Promise.resolve(arr.buffer); - } - - private toBuf(value: string | ArrayBuffer): ArrayBuffer { - let buf: ArrayBuffer; - if (typeof value === "string") { - buf = Utils.fromUtf8ToArray(value).buffer; - } else { - buf = value; - } - return buf; - } - - private toByteString(value: string | ArrayBuffer): string { - let bytes: string; - if (typeof value === "string") { - bytes = forge.util.encodeUtf8(value); - } else { - bytes = Utils.fromBufferToByteString(value); - } - return bytes; - } - - private toWebCryptoAlgorithm(algorithm: "sha1" | "sha256" | "sha512" | "md5"): string { - if (algorithm === "md5") { - throw new Error("MD5 is not supported in WebCrypto."); - } - return algorithm === "sha1" ? "SHA-1" : algorithm === "sha256" ? "SHA-256" : "SHA-512"; - } -} diff --git a/jslib/common/test.setup.ts b/jslib/common/test.setup.ts deleted file mode 100644 index c50c7ca2..00000000 --- a/jslib/common/test.setup.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { webcrypto } from "crypto"; - -import { toEqualBuffer } from "./spec"; - -Object.defineProperty(window, "crypto", { - value: webcrypto, -}); - -// Add custom matchers - -expect.extend({ - toEqualBuffer: toEqualBuffer, -}); - -export interface CustomMatchers { - toEqualBuffer(expected: Uint8Array | ArrayBuffer): R; -} diff --git a/jslib/electron/jest.config.js b/jslib/electron/jest.config.js deleted file mode 100644 index ee692181..00000000 --- a/jslib/electron/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const { pathsToModuleNameMapper } = require("ts-jest"); - -const { compilerOptions } = require("../shared/tsconfig.libs"); - -const sharedConfig = require("../shared/jest.config.ts"); - -/** @type {import('jest').Config} */ -module.exports = { - ...sharedConfig, - displayName: "libs/electron tests", - preset: "ts-jest", - testEnvironment: "node", - setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), -}; diff --git a/jslib/electron/test.setup.ts b/jslib/electron/test.setup.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/jslib/node/jest.config.js b/jslib/node/jest.config.js deleted file mode 100644 index d8dce604..00000000 --- a/jslib/node/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const { pathsToModuleNameMapper } = require("ts-jest"); - -const { compilerOptions } = require("../shared/tsconfig.libs"); - -const sharedConfig = require("../shared/jest.config.ts"); - -/** @type {import('jest').Config} */ -module.exports = { - ...sharedConfig, - displayName: "libs/node tests", - preset: "ts-jest", - testEnvironment: "node", - setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), -}; diff --git a/jslib/node/test.setup.ts b/jslib/node/test.setup.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/jslib/shared/es2020-transformer.ts b/jslib/shared/es2020-transformer.ts deleted file mode 100644 index 3a26e1c0..00000000 --- a/jslib/shared/es2020-transformer.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as ts from "typescript"; - -// Custom Typescript AST transformer for use with ts-jest / jest-preset-angular -// Removes specified ES2020 syntax from source code, as node does not support it yet -// Reference: https://kulshekhar.github.io/ts-jest/docs/getting-started/options/astTransformers -// Use this tool to understand how we identify and filter AST nodes: https://ts-ast-viewer.com/ - -/** - * Remember to increase the version whenever transformer's content is changed. This is to inform Jest to not reuse - * the previous cache which contains old transformer's content - */ -export const version = 1; -export const name = "bit-es2020-transformer"; - -// Returns true for 'import.meta' statements -const isImportMetaStatement = (node: ts.Node) => - ts.isPropertyAccessExpression(node) && - ts.isMetaProperty(node.expression) && - node.expression.keywordToken === ts.SyntaxKind.ImportKeyword; - -export const factory = function (/*opts?: Opts*/) { - function visitor(ctx: ts.TransformationContext, sf: ts.SourceFile) { - const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult => { - if (isImportMetaStatement(node)) { - return null; - } - - // Continue searching child nodes - return ts.visitEachChild(node, visitor, ctx); - }; - return visitor; - } - return (ctx: ts.TransformationContext): ts.Transformer => { - return (sf: ts.SourceFile) => ts.visitNode(sf, visitor(ctx, sf)); - }; -}; diff --git a/jslib/shared/jest.config.angular.js b/jslib/shared/jest.config.angular.js deleted file mode 100644 index ca86d908..00000000 --- a/jslib/shared/jest.config.angular.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-env node */ -/* eslint-disable @typescript-eslint/no-var-requires */ -const { defaultTransformerOptions } = require("jest-preset-angular/presets"); - -/** @type {import('jest').Config} */ -module.exports = { - testMatch: ["**/+(*.)+(spec).+(ts)"], - - // Workaround for a memory leak that crashes tests in CI: - // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 - // Also anecdotally improves performance when run locally - maxWorkers: 3, - - transform: { - "^.+\\.(ts|js|mjs|svg)$": [ - "jest-preset-angular", - { - ...defaultTransformerOptions, - // Jest does not use tsconfig.spec.json by default - tsconfig: "/tsconfig.spec.json", - // Further workaround for memory leak, recommended here: - // https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014 - // Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code - // See https://bitwarden.atlassian.net/browse/EC-497 for more info - isolatedModules: true, - astTransformers: { - before: ["/../../jslib/shared/es2020-transformer.ts"], - }, - }, - ], - }, -}; diff --git a/jslib/shared/jest.config.ts.js b/jslib/shared/jest.config.ts.js deleted file mode 100644 index 970e421a..00000000 --- a/jslib/shared/jest.config.ts.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-env node */ - -/** @type {import('jest').Config} */ -module.exports = { - testMatch: ["**/+(*.)+(spec).+(ts)"], - - // Workaround for a memory leak that crashes tests in CI: - // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 - // Also anecdotally improves performance when run locally - maxWorkers: 3, - - transform: { - "^.+\\.tsx?$": [ - "ts-jest", - { - // Jest does not use tsconfig.spec.json by default - tsconfig: "/tsconfig.spec.json", - // Further workaround for memory leak, recommended here: - // https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014 - // Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code - // See https://bitwarden.atlassian.net/browse/EC-497 for more info - isolatedModules: true, - astTransformers: { - before: ["/../../jslib/shared/es2020-transformer.ts"], - }, - }, - ], - }, -}; diff --git a/jslib/shared/test.environment.ts b/jslib/shared/test.environment.ts deleted file mode 100644 index 404303d1..00000000 --- a/jslib/shared/test.environment.ts +++ /dev/null @@ -1,22 +0,0 @@ -import JSDOMEnvironment from "jest-environment-jsdom"; - -/** - * https://github.com/jsdom/jsdom/issues/3363#issuecomment-1467894943 - * Adds nodes structuredClone implementation to the global object of jsdom. - * use by either adding this file to the testEnvironment property of jest config - * or by adding the following to the top spec file: - * - * ``` - * /** - * * @jest-environment ../shared/test.environment.ts - * *\/ - * ``` - */ -export default class FixJSDOMEnvironment extends JSDOMEnvironment { - constructor(...args: ConstructorParameters) { - super(...args); - - // FIXME https://github.com/jsdom/jsdom/issues/3363 - this.global.structuredClone = structuredClone; - } -} diff --git a/jslib/shared/tsconfig.libs.json b/jslib/shared/tsconfig.libs.json deleted file mode 100644 index 6989026a..00000000 --- a/jslib/shared/tsconfig.libs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "paths": { - "tldjs": ["../common/src/misc/tldjs.noop"], - "jslib-common/*": ["../common/src/*"], - "jslib-angular/*": ["../angular/src/*"], - "jslib-electron/*": ["../electron/src/*"], - "jslib-node/*": ["../node/src/*"] - } - } -} diff --git a/package.json b/package.json index eb579c48..bdc92e47 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "prepare": "husky install", "test": "jest", "test:watch": "jest --watch", - "test:watch:all": "jest --watchAll" + "test:watch:all": "jest --watchAll", + "test:types": "npx tsc --noEmit" }, "devDependencies": { "@angular-devkit/build-angular": "15.2.9", diff --git a/jslib/angular/test.setup.ts b/test.setup.ts similarity index 99% rename from jslib/angular/test.setup.ts rename to test.setup.ts index 6be6e7b8..5a6ff00e 100644 --- a/jslib/angular/test.setup.ts +++ b/test.setup.ts @@ -1,4 +1,5 @@ import { webcrypto } from "crypto"; + import "jest-preset-angular/setup-jest"; Object.defineProperty(window, "CSS", { value: null }); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index bd789b20..bc788099 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -24,5 +24,5 @@ } ] }, - "include": ["src", "jslib", "scripts"] + "include": ["src", "jslib", "scripts", "./*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index e69224ee..133770cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,10 +19,10 @@ "baseUrl": ".", "resolveJsonModule": true, "paths": { - "tldjs": ["@/jslib/src/misc/tldjs.noop"], + "tldjs": ["./jslib/common/src/misc/tldjs.noop"], "@/*": ["./*"] }, "useDefineForClassFields": false }, - "include": ["src", "src-cli", "jslib"] + "include": ["src", "jslib"] }