diff --git a/.github/workflows/shared-ci.yml b/.github/workflows/shared-ci.yml index 88233a595..9d3e94972 100644 --- a/.github/workflows/shared-ci.yml +++ b/.github/workflows/shared-ci.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: ['22.x', '24.x', 'latest'] + node-version: ['18.x', '20.x', '22.x', '24.x', 'latest'] test-type: ['node', 'browser'] # Determine test categories based on whether testing published packages or source code: # - Testing published packages: only run vector tests (don't have build artifacts to test coverage or compliance) diff --git a/modules/web-crypto-backend/package.json b/modules/web-crypto-backend/package.json index 38b70346f..9bb91371a 100644 --- a/modules/web-crypto-backend/package.json +++ b/modules/web-crypto-backend/package.json @@ -19,6 +19,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-crypto/ie11-detection": "4.0.0", "@aws-crypto/supports-web-crypto": "5.2.0", "@aws-sdk/util-locate-window": "3.310.0", "tslib": "^2.2.0" diff --git a/modules/web-crypto-backend/src/backend-factory.ts b/modules/web-crypto-backend/src/backend-factory.ts index df843a86a..289dd8a58 100644 --- a/modules/web-crypto-backend/src/backend-factory.ts +++ b/modules/web-crypto-backend/src/backend-factory.ts @@ -1,12 +1,14 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { isMsWindow } from '@aws-crypto/ie11-detection' import { supportsWebCrypto, supportsSubtleCrypto, supportsZeroByteGCM, } from '@aws-crypto/supports-web-crypto' import { generateSynchronousRandomValues } from './synchronous_random_values' +import promisifyMsSubtleCrypto from './promisify-ms-crypto' type MaybeSubtleCrypto = SubtleCrypto | false export type WebCryptoBackend = @@ -138,6 +140,7 @@ export function pluckSubtleCrypto(window: Window): MaybeSubtleCrypto { // if needed webkitSubtle check should be added here // see: https://webkit.org/blog/7790/update-on-web-cryptography/ if (supportsWebCrypto(window)) return window.crypto.subtle + if (isMsWindow(window)) return promisifyMsSubtleCrypto(window.msCrypto.subtle) return false } diff --git a/modules/web-crypto-backend/src/promisify-ms-crypto.ts b/modules/web-crypto-backend/src/promisify-ms-crypto.ts new file mode 100644 index 000000000..4c17e629f --- /dev/null +++ b/modules/web-crypto-backend/src/promisify-ms-crypto.ts @@ -0,0 +1,38 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { MsSubtleCrypto } from '@aws-crypto/ie11-detection' + +type MsSubtleFunctions = keyof MsSubtleCrypto + +export default function promisifyMsSubtleCrypto(backend: MsSubtleCrypto) { + const usages: MsSubtleFunctions[] = [ + 'decrypt', + 'digest', + 'encrypt', + 'exportKey', + 'generateKey', + 'importKey', + 'sign', + 'verify', + ] + const decorateUsage = (fakeBackend: any, usage: MsSubtleFunctions) => + decorate(backend, fakeBackend, usage) + return usages.reduce(decorateUsage, {}) as SubtleCrypto +} + +function decorate( + subtle: MsSubtleCrypto, + fakeBackend: any, + name: MsSubtleFunctions +) { + fakeBackend[name] = async (...args: any[]) => { + return new Promise((resolve, reject) => { + // @ts-ignore + const operation = subtle[name](...args) + operation.oncomplete = () => resolve(operation.result) + operation.onerror = reject + }) + } + return fakeBackend +} diff --git a/modules/web-crypto-backend/src/synchronous_random_values.ts b/modules/web-crypto-backend/src/synchronous_random_values.ts index 86cfdba3e..0a0147c06 100644 --- a/modules/web-crypto-backend/src/synchronous_random_values.ts +++ b/modules/web-crypto-backend/src/synchronous_random_values.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { isMsWindow } from '@aws-crypto/ie11-detection' import { supportsSecureRandom } from '@aws-crypto/supports-web-crypto' import { locateWindow } from '@aws-sdk/util-locate-window' @@ -18,6 +19,10 @@ export function generateSynchronousRandomValues( return function synchronousRandomValues(byteLength: number): Uint8Array { if (supportsSecureRandom(globalScope)) { return globalScope.crypto.getRandomValues(new Uint8Array(byteLength)) + } else if (isMsWindow(globalScope)) { + const values = new Uint8Array(byteLength) + globalScope.msCrypto.getRandomValues(values) + return values } throw new Error(`Unable to locate a secure random source.`) diff --git a/modules/web-crypto-backend/test/fixtures.ts b/modules/web-crypto-backend/test/fixtures.ts index cc51f9fea..edddc884f 100644 --- a/modules/web-crypto-backend/test/fixtures.ts +++ b/modules/web-crypto-backend/test/fixtures.ts @@ -3,12 +3,7 @@ export const fakeWindowWebCryptoSupportsZeroByteGCM: Window = { crypto: { - getRandomValues: (array: Uint8Array) => { - for (let i = 0; i < array.length; i++) { - array[i] = Math.floor(Math.random() * 256) - } - return array - }, + getRandomValues: () => {}, subtle: { async decrypt() { return {} as any @@ -147,3 +142,145 @@ export const subtleFallbackZeroByteEncryptFail = { } as any export const subtleFallbackNoWebCrypto = {} as any + +export const fakeWindowIE11OnComplete = { + msCrypto: { + getRandomValues: (values: Uint8Array) => { + return values.fill(1) + }, + subtle: { + decrypt() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + digest() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + encrypt() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + exportKey() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + generateKey() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + importKey() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + sign() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + verify() { + const obj = {} as any + setTimeout(() => { + obj.result = true + obj.oncomplete() + }) + return obj + }, + }, + }, + MSInputMethodContext: {} as any, +} as any + +export const fakeWindowIE11OnError = { + msCrypto: { + getRandomValues: (values: Uint8Array) => { + return values.fill(1) + }, + subtle: { + decrypt() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + digest() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + encrypt() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + exportKey() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + generateKey() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + importKey() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + sign() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + verify() { + const obj = {} as any + setTimeout(() => { + obj.onerror(new Error('stub error')) + }) + return obj + }, + }, + }, + MSInputMethodContext: {} as any, +} as any diff --git a/modules/web-crypto-backend/test/promisify-ms-crypto.test.ts b/modules/web-crypto-backend/test/promisify-ms-crypto.test.ts new file mode 100644 index 000000000..7c420478f --- /dev/null +++ b/modules/web-crypto-backend/test/promisify-ms-crypto.test.ts @@ -0,0 +1,36 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import promisifyMsSubtleCrypto from '../src/promisify-ms-crypto' +import * as fixtures from './fixtures' + +chai.use(chaiAsPromised) +const { expect } = chai + +/* These tests are very simple + * I am not testing every subtle function + * because the promisify code is all the same. + */ +describe('promisifyMsSubtleCrypto', () => { + const backendComplete = promisifyMsSubtleCrypto( + fixtures.fakeWindowIE11OnComplete.msCrypto.subtle + ) + const backendError = promisifyMsSubtleCrypto( + fixtures.fakeWindowIE11OnError.msCrypto.subtle + ) + + it('backendComplete:decrypt', async () => { + // @ts-ignore These methods are stubs, ignore ts errors + const test = await backendComplete.decrypt() + expect(test).to.equal(true) + }) + + it('backendError:decrypt', async () => { + // @ts-ignore These methods are stubs, ignore ts errors + await expect(backendError.decrypt()).to.rejectedWith(Error) + }) +}) diff --git a/modules/web-crypto-backend/test/synchronous_random_values.test.ts b/modules/web-crypto-backend/test/synchronous_random_values.test.ts index 8e3a06bf9..1c8d52456 100644 --- a/modules/web-crypto-backend/test/synchronous_random_values.test.ts +++ b/modules/web-crypto-backend/test/synchronous_random_values.test.ts @@ -5,15 +5,25 @@ import { expect } from 'chai' import { generateSynchronousRandomValues } from '../src/synchronous_random_values' +import { synchronousRandomValues } from '../src/index' import * as fixtures from './fixtures' describe('synchronousRandomValues', () => { it('should return random values', () => { + const test = synchronousRandomValues(5) + expect(test).to.be.instanceOf(Uint8Array) + expect(test).lengthOf(5) + }) + + it('should return msCrypto random values', () => { const synchronousRandomValues = generateSynchronousRandomValues( - fixtures.fakeWindowWebCryptoSupportsZeroByteGCM + fixtures.fakeWindowIE11OnComplete ) + const test = synchronousRandomValues(5) expect(test).to.be.instanceOf(Uint8Array) expect(test).lengthOf(5) + // The random is a stub, so I know the value + expect(test).to.deep.equal(new Uint8Array(5).fill(1)) }) }) diff --git a/package-lock.json b/package-lock.json index 09cf950de..419adf3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -715,11 +715,23 @@ "version": "4.0.1", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/ie11-detection": "4.0.0", "@aws-crypto/supports-web-crypto": "5.2.0", "@aws-sdk/util-locate-window": "3.310.0", "tslib": "^2.2.0" } }, + "modules/web-crypto-backend/node_modules/@aws-crypto/ie11-detection": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "modules/web-crypto-backend/node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, "modules/web-crypto-backend/node_modules/@aws-sdk/util-locate-window": { "version": "3.310.0", "license": "Apache-2.0",