Browse Source
* Remove uneeded redirects to a tsconfig.spec.json * Got the tests running again * Delete webCryptoFunctionService * Delete unused utils tests * Add a test workflowpull/400/head
27 changed files with 90 additions and 1300 deletions
@ -0,0 +1,54 @@
@@ -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 |
||||
@ -1,27 +1,42 @@
@@ -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: "<rootDir>/", |
||||
}), |
||||
projects: [ |
||||
"<rootDir>/jslib/angular/jest.config.js", |
||||
"<rootDir>/jslib/common/jest.config.js", |
||||
"<rootDir>/jslib/electron/jest.config.js", |
||||
"<rootDir>/jslib/node/jest.config.js", |
||||
], |
||||
roots: ["<rootDir>"], |
||||
modulePaths: [compilerOptions.baseUrl], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }), |
||||
setupFilesAfterEnv: ["<rootDir>/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, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
@ -1,16 +0,0 @@
@@ -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: ["<rootDir>/test.setup.ts"], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { |
||||
prefix: "<rootDir>/", |
||||
}), |
||||
}; |
||||
@ -1,15 +0,0 @@
@@ -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<R> extends CustomMatchers<R> {} |
||||
interface InverseAsymmetricMatchers extends CustomMatchers {} |
||||
} |
||||
} |
||||
/* eslint-enable */ |
||||
@ -1,17 +0,0 @@
@@ -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: ["<rootDir>/test.setup.ts"], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { |
||||
prefix: "<rootDir>/", |
||||
}), |
||||
}; |
||||
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
export * from "./matchers"; |
||||
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
export * from "./to-equal-buffer"; |
||||
@ -1,25 +0,0 @@
@@ -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); |
||||
}); |
||||
}); |
||||
@ -1,31 +0,0 @@
@@ -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, |
||||
}; |
||||
}; |
||||
@ -1,73 +0,0 @@
@@ -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); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,558 +0,0 @@
@@ -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<PlatformUtilsService>(); |
||||
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; |
||||
} |
||||
@ -1,356 +0,0 @@
@@ -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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
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<boolean> { |
||||
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<string> { |
||||
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<boolean> { |
||||
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<ArrayBuffer> { |
||||
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<string> { |
||||
const p = new DecryptParameters<string>(); |
||||
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<string>): Promise<string> { |
||||
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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
// 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<ArrayBuffer> { |
||||
// 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<ArrayBuffer> { |
||||
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<ArrayBuffer> { |
||||
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"; |
||||
} |
||||
} |
||||
@ -1,17 +0,0 @@
@@ -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<R = unknown> { |
||||
toEqualBuffer(expected: Uint8Array | ArrayBuffer): R; |
||||
} |
||||
@ -1,17 +0,0 @@
@@ -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: ["<rootDir>/test.setup.ts"], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { |
||||
prefix: "<rootDir>/", |
||||
}), |
||||
}; |
||||
@ -1,17 +0,0 @@
@@ -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: ["<rootDir>/test.setup.ts"], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { |
||||
prefix: "<rootDir>/", |
||||
}), |
||||
}; |
||||
@ -1,36 +0,0 @@
@@ -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<any> => { |
||||
if (isImportMetaStatement(node)) { |
||||
return null; |
||||
} |
||||
|
||||
// Continue searching child nodes
|
||||
return ts.visitEachChild(node, visitor, ctx); |
||||
}; |
||||
return visitor; |
||||
} |
||||
return (ctx: ts.TransformationContext): ts.Transformer<any> => { |
||||
return (sf: ts.SourceFile) => ts.visitNode(sf, visitor(ctx, sf)); |
||||
}; |
||||
}; |
||||
@ -1,32 +0,0 @@
@@ -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: "<rootDir>/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: ["<rootDir>/../../jslib/shared/es2020-transformer.ts"], |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
@ -1,29 +0,0 @@
@@ -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: "<rootDir>/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: ["<rootDir>/../../jslib/shared/es2020-transformer.ts"], |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
@ -1,22 +0,0 @@
@@ -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<typeof JSDOMEnvironment>) { |
||||
super(...args); |
||||
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/3363
|
||||
this.global.structuredClone = structuredClone; |
||||
} |
||||
} |
||||
@ -1,12 +0,0 @@
@@ -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/*"] |
||||
} |
||||
} |
||||
} |
||||
@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
|
||||
import { webcrypto } from "crypto"; |
||||
|
||||
import "jest-preset-angular/setup-jest"; |
||||
|
||||
Object.defineProperty(window, "CSS", { value: null }); |
||||
Loading…
Reference in new issue