From 8cff90d3fa34e28b218c97717502b000c101fc13 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Wed, 12 Jun 2024 12:19:38 -0600 Subject: [PATCH] Don't register the message handler with null (#160) * Don't register the message handler with null * Fully functioning bugfix But jest throws a type error. TSC is fine and browsers run it fine, but jsdom+jest blows up * Split worker from its functions so tests compile --- globals.d.ts | 1 - integration/components/InitializeApi.tsx | 4 +- jest.config.js | 4 - package.json | 146 ++++++++-------- src/frame/WorkerLoader.ts | 5 - src/frame/WorkerMediator.ts | 42 ++++- src/frame/worker/WorkerUtil.ts | 161 ++++++++++++++++++ src/frame/worker/index.ts | 207 +---------------------- src/frame/worker/tests/index.test.ts | 52 +++--- tsconfig.json | 48 +++--- 10 files changed, 330 insertions(+), 340 deletions(-) delete mode 100644 src/frame/WorkerLoader.ts create mode 100644 src/frame/worker/WorkerUtil.ts diff --git a/globals.d.ts b/globals.d.ts index 501d3a8..88dfbd1 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -6,7 +6,6 @@ interface Window { msCrypto: Crypto; } - /* * Declare types for modules without build in types */ diff --git a/integration/components/InitializeApi.tsx b/integration/components/InitializeApi.tsx index 98ceeb9..151b0f0 100644 --- a/integration/components/InitializeApi.tsx +++ b/integration/components/InitializeApi.tsx @@ -49,7 +49,7 @@ export default class InitializeApi extends React.Component) => { - if (event.charCode === 13) { + if (event.key === "Enter") { this.setPasscode(); } }; @@ -60,7 +60,7 @@ export default class InitializeApi extends React.Component { + async generateJWT(): Promise { return fetch(`/generateJWT/${window.User.id}`) .then((response) => response.json()) .then((jwt: string) => { diff --git a/jest.config.js b/jest.config.js index 3570b8c..5871f57 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,10 +20,6 @@ module.exports = { diagnostics: { ignoreCodes: [151001], }, - //This can be removed once https://github.com/kulshekhar/ts-jest/issues/1471 is released, probably in ts-jest 25.3.0 - tsconfig: { - outDir: "$$ts-jest$$", - }, }, }, testPathIgnorePatterns: ["/node_modules/", "/nightwatch/", "/protobuf/"], diff --git a/package.json b/package.json index 46f8735..be61870 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,75 @@ { - "license": "AGPL-3.0-only", - "version": "4.2.22", - "scripts": { - "cleanTest": "find dist -type d -name tests -prune -exec rm -rf {} \\;", - "lint": "eslint . --ext .ts,.tsx", - "test": "tsc --noEmit && yarn run lint && yarn run unit --coverage", - "unit": "jest", - "watch": "tsc --noEmit --watch", - "start": "webpack serve --color --config integration/clientHost.webpack.js &! webpack serve --color --config integration/iclHost.webpack.js", - "protocompile": "pbjs -t static-module -w es6 --no-create --no-decode --no-verify --no-convert -o src/frame/protobuf/EncryptedDeks.js src/frame/protobuf/edeks.proto", - "protots": "pbts -o src/frame/protobuf/EncryptedDeks.d.ts src/frame/protobuf/EncryptedDeks.js", - "protobuild": "yarn run protocompile && yarn run protots", - "integrate": "yarn start", - "nightwatch": "nightwatch" - }, - "dependencies": { - "@ironcorelabs/recrypt-wasm-binding": "0.6.0", - "@stablelib/ed25519": "1.0.2", - "@stablelib/utf8": "1.0.1", - "base64-js": "1.5.1", - "fast-text-encoding": "1.0.4", - "futurejs": "2.2.0", - "sjcl": "1.0.8" - }, - "peerDependencies": { - "@stablelib/utf8": "^0.10", - "base64-js": "^1.2", - "futurejs": "^2" - }, - "devDependencies": { - "@types/base64-js": "^1.2.5", - "@types/jest": "^28.1.6", - "@types/material-ui": "^0.21.7", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "@types/react-tap-event-plugin": "^0.0.30", - "@types/sjcl": "^1.0.28", - "@types/webpack-env": "^1.16.0", - "@typescript-eslint/eslint-plugin": "^5.33.0", - "@typescript-eslint/parser": "^5.33.0", - "animal-id": "^0.0.1", - "chromedriver": "^119.0.1", - "cookie-parser": "^1.4.4", - "eslint": "^8.21.0", - "eslint-plugin-react": "^7.19.0", - "express": "^4.19.2", - "jest": "^28.1.3", - "jest-environment-jsdom": "^28.1.3", - "jest-extended": "^3.0.2", - "jsonwebtoken": "^9.0.0", - "material-ui": "^0.20.2", - "nightwatch": "^2.3.0", - "protobufjs": "^7.2.5", - "protobufjs-cli": "^1.0.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "shelljs": "^0.8.5", - "ts-jest": "^28.0.0", - "ts-loader": "^9.3.1", - "typescript": "4.7.4", - "webpack": "^5.76.0", - "webpack-cli": "^4.7.0", - "webpack-dev-server": "^4.10.0", - "worker-loader": "^3.0.8" - }, - "prettier": { - "printWidth": 160, - "tabWidth": 4, - "trailingComma": "es5", - "bracketSpacing": false, - "jsxBracketSameLine": true, - "arrowParens": "always" - } + "license": "AGPL-3.0-only", + "version": "4.2.22", + "scripts": { + "cleanTest": "find dist -type d -name tests -prune -exec rm -rf {} \\;", + "lint": "eslint . --ext .ts,.tsx", + "test": "tsc --noEmit && yarn run lint && yarn run unit --coverage", + "unit": "jest", + "watch": "tsc --noEmit --watch", + "start": "webpack serve --color --config integration/clientHost.webpack.js &! webpack serve --color --config integration/iclHost.webpack.js", + "protocompile": "pbjs -t static-module -w es6 --no-create --no-decode --no-verify --no-convert -o src/frame/protobuf/EncryptedDeks.js src/frame/protobuf/edeks.proto", + "protots": "pbts -o src/frame/protobuf/EncryptedDeks.d.ts src/frame/protobuf/EncryptedDeks.js", + "protobuild": "yarn run protocompile && yarn run protots", + "integrate": "yarn start", + "nightwatch": "nightwatch" + }, + "dependencies": { + "@ironcorelabs/recrypt-wasm-binding": "0.6.0", + "@stablelib/ed25519": "1.0.2", + "@stablelib/utf8": "1.0.1", + "base64-js": "1.5.1", + "fast-text-encoding": "1.0.4", + "futurejs": "2.2.0", + "sjcl": "1.0.8" + }, + "peerDependencies": { + "@stablelib/utf8": "^0.10", + "base64-js": "^1.2", + "futurejs": "^2" + }, + "devDependencies": { + "@types/base64-js": "^1.2.5", + "@types/jest": "^28.1.6", + "@types/material-ui": "^0.21.7", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/react-tap-event-plugin": "^0.0.30", + "@types/sjcl": "^1.0.28", + "@types/webpack-env": "^1.16.0", + "@typescript-eslint/eslint-plugin": "^5.33.0", + "@typescript-eslint/parser": "^5.33.0", + "animal-id": "^0.0.1", + "chromedriver": "^119.0.1", + "cookie-parser": "^1.4.4", + "eslint": "^8.21.0", + "eslint-plugin-react": "^7.19.0", + "express": "^4.19.2", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "jest-extended": "^3.0.2", + "jsonwebtoken": "^9.0.0", + "material-ui": "^0.20.2", + "nightwatch": "^2.3.0", + "protobufjs": "^7.2.5", + "protobufjs-cli": "^1.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "shelljs": "^0.8.5", + "ts-jest": "^28.0.0", + "ts-loader": "^9.3.1", + "typescript": "4.7.4", + "webpack": "^5.76.0", + "webpack-cli": "^4.7.0", + "webpack-dev-server": "^4.10.0", + "worker-loader": "^3.0.8" + }, + "prettier": { + "printWidth": 160, + "tabWidth": 4, + "trailingComma": "es5", + "bracketSpacing": false, + "jsxBracketSameLine": true, + "arrowParens": "always" + } } \ No newline at end of file diff --git a/src/frame/WorkerLoader.ts b/src/frame/WorkerLoader.ts deleted file mode 100644 index 1e30c79..0000000 --- a/src/frame/WorkerLoader.ts +++ /dev/null @@ -1,5 +0,0 @@ -//Simple shim that is responsible for loading a webworker and creating a new instance of it. We're doing this in a shim because we don't want -//this code to run during unit tests and it's much easier to just mock out this whole file -/*global _WORKER_PATH_LOCATION_*/ -const worker: Worker = new Worker(_WORKER_PATH_LOCATION_); -export default worker; diff --git a/src/frame/WorkerMediator.ts b/src/frame/WorkerMediator.ts index da0d340..2ef43f0 100644 --- a/src/frame/WorkerMediator.ts +++ b/src/frame/WorkerMediator.ts @@ -1,15 +1,32 @@ import Future from "futurejs"; +import {ErrorCodes} from "../Constants"; import SDKError from "../lib/SDKError"; import {ErrorResponse, RequestMessage, ResponseMessage} from "../WorkerMessageTypes"; -import worker from "./WorkerLoader"; class WorkerMessenger { readonly worker: Worker; callbackCount = 0; callbacks: {[key: string]: (data: ResponseMessage) => void} = {}; - constructor(workerInstance: Worker) { - this.worker = workerInstance; - worker.addEventListener("message", this.processMessage, false); + workerReady: Promise; + constructor() { + this.worker = new Worker(_WORKER_PATH_LOCATION_); + + // create a promise that will resolve when we receive a "ready" message from our worker. Messages to the + // worker from this messenger will wait for this Promise to be resolved (which once done will stay resolved) + // before sending. + this.workerReady = new Promise((resolve) => { + this.worker.addEventListener( + "message", + (event: MessageEvent) => { + if (event.data == "ready") { + resolve(); + } else { + this.processMessage(event); + } + }, + false + ); + }); } /** @@ -22,10 +39,17 @@ class WorkerMessenger { replyID: this.callbackCount++, data, }; - this.worker.postMessage(message, transferList.map((intByteArray) => intByteArray.buffer)); - return new Future((_, resolve) => { - this.callbacks[message.replyID] = resolve; - }); + return Future.tryP(() => this.workerReady) + .errorMap((e) => new SDKError(e, ErrorCodes.FRAME_LOAD_FAILURE)) + .flatMap(() => { + this.worker.postMessage( + message, + transferList.map((intByteArray) => intByteArray.buffer) + ); + return new Future((_, resolve) => { + this.callbacks[message.replyID] = resolve; + }); + }); } /** @@ -42,7 +66,7 @@ class WorkerMessenger { }; } -export const messenger = new WorkerMessenger(worker); +export const messenger = new WorkerMessenger(); /** * Type guard to check if returned message is an error message type diff --git a/src/frame/worker/WorkerUtil.ts b/src/frame/worker/WorkerUtil.ts new file mode 100644 index 0000000..92154bd --- /dev/null +++ b/src/frame/worker/WorkerUtil.ts @@ -0,0 +1,161 @@ +import SDKError from "../../lib/SDKError"; +import {ErrorResponse, RequestMessage, ResponseMessage} from "../../WorkerMessageTypes"; +import * as DocumentCrypto from "./DocumentCrypto"; +import * as GroupCrypto from "./GroupCrypto"; +import * as SearchCrypto from "./SearchCrypto"; +import * as UserCrypto from "./UserCrypto"; + +//The postMessage function in a WebWorker has a different signature than the frame postMessage function. The "correct" fix here would be to include +//the "webworker" lib in the tsconfig file. But you can't use that in conjunction with the "dom" lib, so everything breaks down. So instead of trying +//to do tons of work to hack those together, we're just redefining this method to match the signature we know it has. +declare function postMessage(message: any, transfer?: ArrayBuffer[]): void; + +/** + * Generic method to convert SDK error messages down into the message/code parts for transfer back to the parent window + */ +function errorResponse(callback: (response: ErrorResponse) => void, error: SDKError) { + callback({ + type: "ERROR_RESPONSE", + message: { + code: error.code, + text: error.message, + }, + }); +} + +/* tslint:disable cyclomatic-complexity */ +export const onMessageCallback = (data: RequestMessage, callback: (message: ResponseMessage, transferList?: Uint8Array[]) => void): void => { + const errorHandler = errorResponse.bind(null, callback); + switch (data.type) { + case "USER_DEVICE_KEYGEN": + const {message} = data; + return UserCrypto.generateDeviceAndSigningKeys( + message.jwtToken, + message.passcode, + message.keySalt, + message.encryptedPrivateUserKey, + message.publicUserKey + ).engage(errorHandler, (keys) => callback({type: "USER_DEVICE_KEYGEN_RESPONSE", message: keys})); + case "NEW_USER_AND_DEVICE_KEYGEN": + return UserCrypto.generateNewUserAndDeviceKeys(data.message.passcode).engage(errorHandler, (keys) => + callback({type: "NEW_USER_AND_DEVICE_KEYGEN_RESPONSE", message: keys}) + ); + case "NEW_USER_KEYGEN": + return UserCrypto.generateNewUserKeys(data.message.passcode).engage(errorHandler, (keys) => + callback({type: "NEW_USER_KEYGEN_RESPONSE", message: keys}) + ); + case "DECRYPT_LOCAL_KEYS": + return UserCrypto.decryptDeviceAndSigningKeys( + data.message.encryptedDeviceKey, + data.message.encryptedSigningKey, + data.message.symmetricKey, + data.message.nonce + ).engage(errorHandler, (deviceAndSigningKeys) => callback({type: "DECRYPT_LOCAL_KEYS_RESPONSE", message: deviceAndSigningKeys})); + case "ROTATE_USER_PRIVATE_KEY": + return UserCrypto.rotatePrivateKey(data.message.passcode, data.message.encryptedPrivateUserKey).engage(errorHandler, (userRotationResult) => + callback({type: "ROTATE_USER_PRIVATE_KEY_RESPONSE", message: userRotationResult}) + ); + case "CHANGE_USER_PASSCODE": + return UserCrypto.changeUsersPasscode(data.message.currentPasscode, data.message.newPasscode, data.message.encryptedPrivateUserKey).engage( + errorHandler, + (encryptedPrivateKey) => callback({type: "CHANGE_USER_PASSCODE_RESPONSE", message: encryptedPrivateKey}) + ); + case "SIGNATURE_GENERATION": { + const signature = UserCrypto.signRequestPayload( + data.message.segmentID, + data.message.userID, + data.message.signingKeys, + data.message.method, + data.message.url, + data.message.body + ); + return callback({type: "SIGNATURE_GENERATION_RESPONSE", message: signature}); + } + case "DOCUMENT_ENCRYPT": + return DocumentCrypto.encryptDocument(data.message.document, data.message.userKeyList, data.message.groupKeyList, data.message.signingKeys).engage( + errorHandler, + (encryptedContent) => callback({type: "DOCUMENT_ENCRYPT_RESPONSE", message: encryptedContent}, [encryptedContent.encryptedDocument.content]) + ); + case "DOCUMENT_DECRYPT": + return DocumentCrypto.decryptDocument(data.message.document, data.message.encryptedSymmetricKey, data.message.privateKey).engage( + errorHandler, + (decryptedDocument) => callback({type: "DOCUMENT_DECRYPT_RESPONSE", message: {decryptedDocument}}, [decryptedDocument]) + ); + case "DOCUMENT_REENCRYPT": + return DocumentCrypto.reEncryptDocument(data.message.document, data.message.existingDocumentSymmetricKey, data.message.privateKey).engage( + errorHandler, + (encryptedDocument) => callback({type: "DOCUMENT_REENCRYPT_RESPONSE", message: {encryptedDocument}}, [encryptedDocument.content]) + ); + case "DOCUMENT_ENCRYPT_TO_KEYS": + return DocumentCrypto.encryptToKeys( + data.message.symmetricKey, + data.message.userKeyList, + data.message.groupKeyList, + data.message.privateKey, + data.message.signingKeys + ).engage(errorHandler, (keyList) => callback({type: "DOCUMENT_ENCRYPT_TO_KEYS_RESPONSE", message: keyList})); + case "GROUP_CREATE": + return GroupCrypto.createGroup(data.message.signingKeys, data.message.memberList, data.message.adminList).engage(errorHandler, (group) => + callback({type: "GROUP_CREATE_RESPONSE", message: group}) + ); + case "ROTATE_GROUP_PRIVATE_KEY": + return GroupCrypto.rotatePrivateKey( + data.message.encryptedGroupKey, + data.message.adminList, + data.message.userPrivateMasterKey, + data.message.signingKeys + ).engage(errorHandler, (result) => callback({type: "ROTATE_GROUP_PRIVATE_KEY_RESPONSE", message: result})); + case "GROUP_ADD_ADMINS": + return GroupCrypto.addAdminsToGroup( + data.message.encryptedGroupKey, + data.message.groupPublicKey, + data.message.groupID, + data.message.userKeyList, + data.message.adminPrivateKey, + data.message.signingKeys + ).engage(errorHandler, (result) => callback({type: "GROUP_ADD_ADMINS_RESPONSE", message: result})); + case "GROUP_ADD_MEMBERS": + return GroupCrypto.addMembersToGroup( + data.message.encryptedGroupKey, + data.message.groupPublicKey, + data.message.groupID, + data.message.userKeyList, + data.message.adminPrivateKey, + data.message.signingKeys + ).engage(errorHandler, (result) => callback({type: "GROUP_ADD_MEMBERS_RESPONSE", message: result})); + case "SEARCH_TOKENIZE_DATA": { + const message = SearchCrypto.tokenizeData(data.message.value, data.message.salt, data.message.partitionId); + return callback({type: "SEARCH_TOKENIZE_STRING_RESPONSE", message}); + } + case "SEARCH_TOKENIZE_QUERY": { + const message = SearchCrypto.tokenizeQuery(data.message.value, data.message.salt, data.message.partitionId); + return callback({type: "SEARCH_TOKENIZE_STRING_RESPONSE", message}); + } + case "SEARCH_TRANSLITERATE_STRING": { + const message = SearchCrypto.transliterateString(data.message); + return callback({type: "SEARCH_TRANSLITERATE_STRING_RESPONSE", message}); + } + default: + //Force TS to tell us if we ever create a new request type that we don't handle here + const exhaustiveCheck: never = data; + return exhaustiveCheck; + } +}; + +export const postMessageToParent = (data: ResponseMessage, replyID: number, transferList: Uint8Array[] = []) => { + const message: WorkerEvent = { + replyID, + data, + }; + postMessage( + message, + transferList.map((intByteArray) => intByteArray.buffer) + ); +}; + +export const processMessageIntoWorker = (event: MessageEvent) => { + const message: WorkerEvent = event.data; + onMessageCallback(message.data, (responseData: ResponseMessage, transferList?: Uint8Array[]) => { + postMessageToParent(responseData, message.replyID, transferList); + }); +}; diff --git a/src/frame/worker/index.ts b/src/frame/worker/index.ts index 6a6671e..4ad6dd8 100644 --- a/src/frame/worker/index.ts +++ b/src/frame/worker/index.ts @@ -1,201 +1,10 @@ -import SDKError from "../../lib/SDKError"; -import {ErrorResponse, RequestMessage, ResponseMessage} from "../../WorkerMessageTypes"; -import * as DocumentCrypto from "./DocumentCrypto"; -import * as GroupCrypto from "./GroupCrypto"; -import * as SearchCrypto from "./SearchCrypto"; -import * as UserCrypto from "./UserCrypto"; +import {processMessageIntoWorker} from "./WorkerUtil"; -type WorkerMessageCallback = (message: RequestMessage, callback: (response: ResponseMessage, transferList?: Uint8Array[]) => void) => void; +// set our listening chain up, so we trigger our pipeline as we receive messages +onmessage = processMessageIntoWorker; -//The postMessage function in a WebWorker has a different signature than the frame postMessage function. The "correct" fix here would be to include -//the "webworker" lib in the tsconfig file. But you can't use that in conjunction with the "dom" lib, so everything breaks down. So instead of trying -//to do tons of work to hack those together, we're just redefining this method to match the signature we know it has. -declare function postMessage(message: any, transfer?: ArrayBuffer[]): void; - -class ParentThreadMessenger { - onMessageCallback!: WorkerMessageCallback; - constructor() { - self.addEventListener("message", this.processMessageIntoWorker, false); - } - - /** - * Subscribe a callback to when a new message is posted to this frame - */ - onMessage(callback: WorkerMessageCallback) { - this.onMessageCallback = callback; - } - - /** - * Post a response message back to the parent window - * @param {ResponseMessage} data Response message to post - * @param {number} replyID ID of original message into frame. Used to invoke the proper callback on the parent window - * @param {Uint8Array[]} transferList List of Uint8Arrays to transfer to parent - */ - postMessageToParent(data: ResponseMessage, replyID: number, transferList: Uint8Array[] = []) { - const message: WorkerEvent = { - replyID, - data, - }; - postMessage( - message, - transferList.map((intByteArray) => intByteArray.buffer) - ); - } - - /** - * Process a received message into the iFrame - * @param {MessageEvent} event Frame postMessage event object - */ - processMessageIntoWorker = (event: MessageEvent) => { - const message: WorkerEvent = event.data; - this.onMessageCallback(message.data, (responseData: ResponseMessage, transferList?: Uint8Array[]) => { - this.postMessageToParent(responseData, message.replyID, transferList); - }); - }; -} - -export const messenger = new ParentThreadMessenger(); - -/** - * Generic method to convert SDK error messages down into the message/code parts for transfer back to the parent window - */ -function errorResponse(callback: (response: ErrorResponse) => void, error: SDKError) { - callback({ - type: "ERROR_RESPONSE", - message: { - code: error.code, - text: error.message, - }, - }); -} - -/* tslint:disable cyclomatic-complexity */ -messenger.onMessage((data: RequestMessage, callback: (message: ResponseMessage, transferList?: Uint8Array[]) => void): void => { - const errorHandler = errorResponse.bind(null, callback); - switch (data.type) { - case "USER_DEVICE_KEYGEN": - const {message} = data; - return UserCrypto.generateDeviceAndSigningKeys( - message.jwtToken, - message.passcode, - message.keySalt, - message.encryptedPrivateUserKey, - message.publicUserKey - ).engage(errorHandler, (keys) => callback({type: "USER_DEVICE_KEYGEN_RESPONSE", message: keys})); - case "NEW_USER_AND_DEVICE_KEYGEN": - return UserCrypto.generateNewUserAndDeviceKeys(data.message.passcode).engage(errorHandler, (keys) => - callback({type: "NEW_USER_AND_DEVICE_KEYGEN_RESPONSE", message: keys}) - ); - case "NEW_USER_KEYGEN": - return UserCrypto.generateNewUserKeys(data.message.passcode).engage(errorHandler, (keys) => - callback({type: "NEW_USER_KEYGEN_RESPONSE", message: keys}) - ); - case "DECRYPT_LOCAL_KEYS": - return UserCrypto.decryptDeviceAndSigningKeys( - data.message.encryptedDeviceKey, - data.message.encryptedSigningKey, - data.message.symmetricKey, - data.message.nonce - ).engage(errorHandler, (deviceAndSigningKeys) => callback({type: "DECRYPT_LOCAL_KEYS_RESPONSE", message: deviceAndSigningKeys})); - case "ROTATE_USER_PRIVATE_KEY": - return UserCrypto.rotatePrivateKey(data.message.passcode, data.message.encryptedPrivateUserKey).engage(errorHandler, (userRotationResult) => - callback({type: "ROTATE_USER_PRIVATE_KEY_RESPONSE", message: userRotationResult}) - ); - case "CHANGE_USER_PASSCODE": - return UserCrypto.changeUsersPasscode( - data.message.currentPasscode, - data.message.newPasscode, - data.message.encryptedPrivateUserKey - ).engage(errorHandler, (encryptedPrivateKey) => callback({type: "CHANGE_USER_PASSCODE_RESPONSE", message: encryptedPrivateKey})); - case "SIGNATURE_GENERATION": - { - const signature = UserCrypto.signRequestPayload( - data.message.segmentID, - data.message.userID, - data.message.signingKeys, - data.message.method, - data.message.url, - data.message.body - ); - return callback({type: "SIGNATURE_GENERATION_RESPONSE", message: signature}); - } - case "DOCUMENT_ENCRYPT": - return DocumentCrypto.encryptDocument( - data.message.document, - data.message.userKeyList, - data.message.groupKeyList, - data.message.signingKeys - ).engage(errorHandler, (encryptedContent) => - callback({type: "DOCUMENT_ENCRYPT_RESPONSE", message: encryptedContent}, [encryptedContent.encryptedDocument.content]) - ); - case "DOCUMENT_DECRYPT": - return DocumentCrypto.decryptDocument(data.message.document, data.message.encryptedSymmetricKey, data.message.privateKey).engage( - errorHandler, - (decryptedDocument) => callback({type: "DOCUMENT_DECRYPT_RESPONSE", message: {decryptedDocument}}, [decryptedDocument]) - ); - case "DOCUMENT_REENCRYPT": - return DocumentCrypto.reEncryptDocument( - data.message.document, - data.message.existingDocumentSymmetricKey, - data.message.privateKey - ).engage(errorHandler, (encryptedDocument) => - callback({type: "DOCUMENT_REENCRYPT_RESPONSE", message: {encryptedDocument}}, [encryptedDocument.content]) - ); - case "DOCUMENT_ENCRYPT_TO_KEYS": - return DocumentCrypto.encryptToKeys( - data.message.symmetricKey, - data.message.userKeyList, - data.message.groupKeyList, - data.message.privateKey, - data.message.signingKeys - ).engage(errorHandler, (keyList) => callback({type: "DOCUMENT_ENCRYPT_TO_KEYS_RESPONSE", message: keyList})); - case "GROUP_CREATE": - return GroupCrypto.createGroup(data.message.signingKeys, data.message.memberList, data.message.adminList).engage(errorHandler, (group) => - callback({type: "GROUP_CREATE_RESPONSE", message: group}) - ); - case "ROTATE_GROUP_PRIVATE_KEY": - return GroupCrypto.rotatePrivateKey( - data.message.encryptedGroupKey, - data.message.adminList, - data.message.userPrivateMasterKey, - data.message.signingKeys - ).engage(errorHandler, (result) => callback({type: "ROTATE_GROUP_PRIVATE_KEY_RESPONSE", message: result})); - case "GROUP_ADD_ADMINS": - return GroupCrypto.addAdminsToGroup( - data.message.encryptedGroupKey, - data.message.groupPublicKey, - data.message.groupID, - data.message.userKeyList, - data.message.adminPrivateKey, - data.message.signingKeys - ).engage(errorHandler, (result) => callback({type: "GROUP_ADD_ADMINS_RESPONSE", message: result})); - case "GROUP_ADD_MEMBERS": - return GroupCrypto.addMembersToGroup( - data.message.encryptedGroupKey, - data.message.groupPublicKey, - data.message.groupID, - data.message.userKeyList, - data.message.adminPrivateKey, - data.message.signingKeys - ).engage(errorHandler, (result) => callback({type: "GROUP_ADD_MEMBERS_RESPONSE", message: result})); - case "SEARCH_TOKENIZE_DATA": - { - const message = SearchCrypto.tokenizeData(data.message.value, data.message.salt, data.message.partitionId); - return callback({type: "SEARCH_TOKENIZE_STRING_RESPONSE", message}); - } - case "SEARCH_TOKENIZE_QUERY": - { - const message = SearchCrypto.tokenizeQuery(data.message.value, data.message.salt, data.message.partitionId); - return callback({type: "SEARCH_TOKENIZE_STRING_RESPONSE", message}); - } - case "SEARCH_TRANSLITERATE_STRING": - { - const message = SearchCrypto.transliterateString(data.message); - return callback({type: "SEARCH_TRANSLITERATE_STRING_RESPONSE", message}); - } - default: - //Force TS to tell us if we ever create a new request type that we don't handle here - const exhaustiveCheck: never = data; - return exhaustiveCheck; - } -}); +// now that everything is set up, send a ready message to our listener. This avoids us being sent messages before we +// can handle them, reducing the risk we'll drop messages +// jest/jsdom really throw a fit about this line while TSC is fine with it. WorkerUtil contains most of the logic only +// to get around this issue. +postMessage("ready"); diff --git a/src/frame/worker/tests/index.test.ts b/src/frame/worker/tests/index.test.ts index 406bf7e..2d997ed 100644 --- a/src/frame/worker/tests/index.test.ts +++ b/src/frame/worker/tests/index.test.ts @@ -1,17 +1,17 @@ import Future from "futurejs"; -import {messenger} from "../"; +import * as worker from "../WorkerUtil"; import SDKError from "../../../lib/SDKError"; import * as DocumentCrypto from "../DocumentCrypto"; import * as GroupCrypto from "../GroupCrypto"; import * as UserCrypto from "../UserCrypto"; import {fromByteArray} from "base64-js"; -describe("worker index", () => { - describe("ParentThreadMessenger", () => { +describe("worker", () => { + describe("utils", () => { it("posts proper worker message to parent window", () => { jest.spyOn(window, "postMessage").mockImplementation(); - messenger.postMessageToParent({foo: "bar"} as any, 10); + worker.postMessageToParent({foo: "bar"} as any, 10); expect(window.postMessage).toHaveBeenCalledWith( { @@ -26,7 +26,7 @@ describe("worker index", () => { jest.spyOn(window, "postMessage").mockImplementation(); const bytes = new Uint8Array(3); - messenger.postMessageToParent({foo: "bar"} as any, 10, [bytes]); + worker.postMessageToParent({foo: "bar"} as any, 10, [bytes]); expect(window.postMessage).toHaveBeenCalledWith( { @@ -40,12 +40,12 @@ describe("worker index", () => { it("invokes message callback with event data when processing", () => { jest.spyOn(window, "postMessage").mockImplementation(); const bytes = new Uint8Array(3); - jest.spyOn(messenger, "onMessageCallback"); + jest.spyOn(worker, "onMessageCallback"); - messenger.processMessageIntoWorker({data: {data: {foo: "bar"}, replyID: 38}} as MessageEvent); + worker.processMessageIntoWorker({data: {data: {foo: "bar"}, replyID: 38}} as MessageEvent); - expect(messenger.onMessageCallback).toHaveBeenCalledWith({foo: "bar"}, expect.any(Function)); - const callback = (messenger.onMessageCallback as unknown as jest.SpyInstance).mock.calls[0][1]; + expect(worker.onMessageCallback).toHaveBeenCalledWith({foo: "bar"}, expect.any(Function)); + const callback = (worker.onMessageCallback as unknown as jest.SpyInstance).mock.calls[0][1]; callback({response: "data"}, [bytes]); expect(window.postMessage).toHaveBeenCalledWith( @@ -74,7 +74,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: expect.any(String), message: "new keys"}); expect(UserCrypto.generateDeviceAndSigningKeys).toHaveBeenCalledWith("jwt", "passcode", "salt", "user key", "public user key"); done(); @@ -90,7 +90,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: expect.any(String), message: "new keys"}); expect(UserCrypto.generateNewUserKeys).toHaveBeenCalledWith("passcode"); done(); @@ -106,7 +106,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: expect.any(String), message: "new keys"}); expect(UserCrypto.generateNewUserAndDeviceKeys).toHaveBeenCalledWith("passcode"); done(); @@ -125,7 +125,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: expect.any(String), message: "decrypted keys"}); expect(UserCrypto.decryptDeviceAndSigningKeys).toHaveBeenCalledWith("device", "signing", "sym key", "nonce"); done(); @@ -138,7 +138,7 @@ describe("worker index", () => { type: "ROTATE_USER_PRIVATE_KEY", message: {passcode: "passcode", encryptedPrivateUserKey: "encryptedPrivateUserKey"}, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: "ROTATE_USER_PRIVATE_KEY_RESPONSE", message: "rotated user key"}); expect(UserCrypto.rotatePrivateKey).toHaveBeenCalledWith("passcode", "encryptedPrivateUserKey"); done(); @@ -157,7 +157,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: "CHANGE_USER_PASSCODE_RESPONSE", message: "new encrypted private key"}); expect(UserCrypto.changeUsersPasscode).toHaveBeenCalledWith("current", "new", "current encrypted private key"); done(); @@ -177,7 +177,7 @@ describe("worker index", () => { body: '{"foo":"bar"}', }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({type: "SIGNATURE_GENERATION_RESPONSE", message: "signature"}); expect(UserCrypto.signRequestPayload).toHaveBeenCalledWith( 1, @@ -211,7 +211,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any, transferList: any) => { + worker.onMessageCallback!(payload, (result: any, transferList: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual(encryptedDoc); expect(transferList).toEqual([encryptedDoc.encryptedDocument.content]); @@ -232,7 +232,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any, transferList: any) => { + worker.onMessageCallback!(payload, (result: any, transferList: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual({decryptedDocument: "decrypted doc"}); expect(transferList).toEqual(["decrypted doc"]); @@ -256,7 +256,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any, transferList: any) => { + worker.onMessageCallback!(payload, (result: any, transferList: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual({encryptedDocument: encryptedDoc}); expect(transferList).toEqual([encryptedDoc.content]); @@ -279,7 +279,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual("key list"); expect(DocumentCrypto.encryptToKeys).toHaveBeenCalledWith("sym key", "user grant list", "group grant list", "priv key", "signkeys"); @@ -302,7 +302,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual("created group"); expect(GroupCrypto.createGroup).toHaveBeenCalledWith("signkeys", [creator], [creator]); @@ -324,7 +324,7 @@ describe("worker index", () => { signingKeys: "signkeys", }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual("rotate group key"); expect(GroupCrypto.rotatePrivateKey).toHaveBeenCalledWith({foo: "bar"}, ["32", "13"], new Uint8Array(32), "signkeys"); @@ -349,7 +349,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual("added admins"); expect(GroupCrypto.addAdminsToGroup).toHaveBeenCalledWith( @@ -381,7 +381,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result.type).toEqual(expect.any(String)); expect(result.message).toEqual("added members"); expect(GroupCrypto.addMembersToGroup).toHaveBeenCalledWith( @@ -409,7 +409,7 @@ describe("worker index", () => { }, }; - messenger.onMessageCallback!(payload, (result: any) => { + worker.onMessageCallback!(payload, (result: any) => { expect(result).toEqual({ type: "ERROR_RESPONSE", message: { @@ -422,7 +422,7 @@ describe("worker index", () => { }); it("if you bypass typescript's checks you just get your original message back", () => { - messenger.onMessageCallback({type: "UNKNOWN", message: "data"} as any, (result: any) => expect(result).toBe("data")); + worker.onMessageCallback({type: "UNKNOWN", message: "data"} as any, (result: any) => expect(result).toBe("data")); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index adf1a50..66810ec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,28 @@ { - "compilerOptions": { - "strict": true, - "sourceMap": true, - "noImplicitReturns": true, - "noUnusedParameters": true, - "downlevelIteration": true, - "noEmitOnError": true, - "noUnusedLocals": true, - "declaration": false, - "jsx": "react", - "module": "esNext", - "target": "ES5", - "allowJs": true, - "baseUrl": ".", - "moduleResolution": "node", - "pretty": true, - "removeComments": false, - "lib": ["es6", "dom"] - }, - "include": ["src/**/*", "globals.d.ts"] -} + "compilerOptions": { + "strict": true, + "sourceMap": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "downlevelIteration": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "declaration": false, + "jsx": "react", + "module": "esNext", + "target": "ES5", + "allowJs": true, + "baseUrl": ".", + "moduleResolution": "node", + "pretty": true, + "removeComments": false, + "lib": [ + "es6", + "dom" + ] + }, + "include": [ + "src/**/*", + "globals.d.ts" + ] +} \ No newline at end of file