Skip to content

Commit

Permalink
Split worker from its functions so tests compile
Browse files Browse the repository at this point in the history
  • Loading branch information
skeet70 committed Jun 12, 2024
1 parent ca590b2 commit 3d168e8
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 168 deletions.
4 changes: 0 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/"],
Expand Down
161 changes: 161 additions & 0 deletions src/frame/worker/WorkerUtil.ts
Original file line number Diff line number Diff line change
@@ -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<ResponseMessage> = {
replyID,
data,
};
postMessage(
message,
transferList.map((intByteArray) => intByteArray.buffer)
);
};

export const processMessageIntoWorker = (event: MessageEvent) => {
const message: WorkerEvent<RequestMessage> = event.data;
onMessageCallback(message.data, (responseData: ResponseMessage, transferList?: Uint8Array[]) => {
postMessageToParent(responseData, message.replyID, transferList);
});
};
164 changes: 3 additions & 161 deletions src/frame/worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,168 +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";

//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<ResponseMessage> = {
replyID,
data,
};
postMessage(
message,
transferList.map((intByteArray) => intByteArray.buffer)
);
};

export const processMessageIntoWorker = (event: MessageEvent) => {
const message: WorkerEvent<RequestMessage> = event.data;
onMessageCallback(message.data, (responseData: ResponseMessage, transferList?: Uint8Array[]) => {
postMessageToParent(responseData, message.replyID, transferList);
});
};
import {processMessageIntoWorker} from "./WorkerUtil";

// set our listening chain up, so we trigger our pipeline as we receive messages
onmessage = processMessageIntoWorker;

// 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");
6 changes: 3 additions & 3 deletions src/frame/worker/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Future from "futurejs";
import * as worker 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();

Expand Down

0 comments on commit 3d168e8

Please sign in to comment.