diff --git a/background.html b/background.html index 93d30a21a..b14e5ee3f 100644 --- a/background.html +++ b/background.html @@ -26,7 +26,9 @@ Session - + @@ -40,6 +42,9 @@ + diff --git a/package.json b/package.json index 1dde93083..0764d7eea 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "name": "Oxen Labs", "email": "team@oxen.io" }, - "repository": { "type": "git", "url": "https://github.com/oxen-io/session-desktop.git" diff --git a/preload.js b/preload.js index 3975e4f7d..f8ada49b5 100644 --- a/preload.js +++ b/preload.js @@ -204,15 +204,8 @@ window.nodeSetImmediate = setImmediate; const data = require('./ts/data/dataInit'); const { setupi18n } = require('./ts/util/i18n'); window.Signal = data.initData(); -const { WorkerInterface } = require('./ts/node/util_worker_interface'); -// A Worker with a 3 minute timeout -// console.warn('app', require('electron/main').app); -// const utilWorkerPath = path.join(app.getAppPath(), 'js', 'util_worker.js'); -// const utilWorker = new WorkerInterface(utilWorkerPath, 3 * 60 * 1000); - -window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { // tslint:disable-next-line: no-empty diff --git a/ts/attachments/attachments.ts b/ts/attachments/attachments.ts index 6fc7f5cb1..f9308bb0f 100644 --- a/ts/attachments/attachments.ts +++ b/ts/attachments/attachments.ts @@ -8,7 +8,7 @@ import { isArrayBuffer, isString, map } from 'lodash'; import { decryptAttachmentBuffer, encryptAttachmentBuffer, -} from '../node/local_attachments_encrypter'; +} from '../util/local_attachments_encrypter'; const PATH = 'attachments.noindex'; diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx index f98205793..bc1c6735e 100644 --- a/ts/components/dialog/SessionSeedModal.tsx +++ b/ts/components/dialog/SessionSeedModal.tsx @@ -26,7 +26,7 @@ const Password = (props: PasswordProps) => { const onClose = () => dispatch(recoveryPhraseModal(null)); const confirmPassword = () => { - const passwordValue = jQuery('#seed-input-password').val(); + const passwordValue = (document.getElementById('seed-input-password') as any)?.val(); const isPasswordValid = matchesHash(passwordValue as string, passwordHash); if (!passwordValue) { @@ -145,7 +145,7 @@ const SessionSeedModalInner = (props: ModalInnerProps) => { const dispatch = useDispatch(); useEffect(() => { - setTimeout(() => (jQuery('#seed-input-password') as any).focus(), 100); + setTimeout(() => (document.getElementById('seed-input-password') as any)?.focus(), 100); void checkHasPassword(); void getRecoveryPhrase(); }, []); diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 6786cf515..189f86103 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -53,7 +53,6 @@ import { SessionToastContainer } from '../SessionToastContainer'; import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; import { getLatestDesktopReleaseFileToFsV2 } from '../../session/apis/file_server_api/FileServerApiV2'; import { ipcRenderer } from 'electron'; -import { yo } from '../../webworker/master'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); @@ -260,8 +259,6 @@ const doAppStartUp = () => { void loadDefaultRooms(); - void yo(); - debounce(triggerAvatarReUploadIfNeeded, 200); }; diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index 950b9bcf1..a7b0ab68f 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -518,7 +518,9 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => { }; export function isRtlBody(): boolean { - return (jQuery('body') as any).hasClass('rtl'); + const body = document.getElementsByTagName('body').item(0); + + return body?.classList.contains('rtl') || false; } export const BlockMenuItem = (): JSX.Element | null => { diff --git a/ts/components/settings/SessionSettings.tsx b/ts/components/settings/SessionSettings.tsx index d8eabee5a..3bf38e389 100644 --- a/ts/components/settings/SessionSettings.tsx +++ b/ts/components/settings/SessionSettings.tsx @@ -110,7 +110,7 @@ export class SessionSettingsView extends React.Component (jQuery('#password-lock-input') as any).focus(), 100); + setTimeout(() => (document.getElementById('password-lock-input') as any)?.focus(), 100); } public componentWillUnmount() { @@ -149,7 +149,7 @@ export class SessionSettingsView extends React.Component { const job = _getJob(jobId); if (!job) { diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index 5769938e3..222e063f2 100644 --- a/ts/mains/main_node.ts +++ b/ts/mains/main_node.ts @@ -484,7 +484,7 @@ async function showPasswordWindow() { nodeIntegration: false, enableRemoteModule: true, nodeIntegrationInWorker: false, - contextIsolation: true, + contextIsolation: false, // sandbox: true, preload: path.join(__dirname, '..', 'password_preload.js'), diff --git a/ts/mains/main_renderer.tsx b/ts/mains/main_renderer.tsx index 6fdf12c89..46a6e2a92 100644 --- a/ts/mains/main_renderer.tsx +++ b/ts/mains/main_renderer.tsx @@ -110,7 +110,6 @@ Storage.onready(async () => { return; } first = false; - console.warn('storage is ready'); // Update zoom window.updateZoomFactor(); diff --git a/ts/node/util_worker_interface.ts b/ts/node/util_worker_interface.ts index e88a7665b..2ac60bc4d 100644 --- a/ts/node/util_worker_interface.ts +++ b/ts/node/util_worker_interface.ts @@ -1,122 +1 @@ /* global Worker, window, setTimeout */ - -const WORKER_TIMEOUT = 60 * 1000; // one minute - -class TimedOutError extends Error { - constructor(message: string) { - super(message); - this.name = this.constructor.name; - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = new Error(message).stack; - } - } -} - -export class WorkerInterface { - private readonly timeout: number; - private readonly _DEBUG: boolean; - private _jobCounter: number; - private readonly _jobs: Record; - private readonly _utilWorker: Worker; - - constructor(path: string, timeout = WORKER_TIMEOUT) { - this._utilWorker = new Worker(path); - this.timeout = timeout; - this._jobs = Object.create(null); - this._DEBUG = false; - this._jobCounter = 0; - - this._utilWorker.onmessage = e => { - const [jobId, errorForDisplay, result] = e.data; - - const job = this._getJob(jobId); - if (!job) { - throw new Error( - `Received worker reply to job ${jobId}, but did not have it in our registry!` - ); - } - - const { resolve, reject, fnName } = job; - - if (errorForDisplay) { - return reject( - new Error(`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`) - ); - } - - return resolve(result); - }; - } - - public async callWorker(fnName: string, ...args: any) { - const jobId = this._makeJob(fnName); - - return new Promise((resolve, reject) => { - this._utilWorker.postMessage([jobId, fnName, ...args]); - - this._updateJob(jobId, { - resolve, - reject, - args: this._DEBUG ? args : null, - }); - - setTimeout(() => { - reject(new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)); - }, this.timeout); - }); - } - - private _makeJob(fnName: string): number { - this._jobCounter += 1; - const id = this._jobCounter; - - if (this._DEBUG) { - window.log.info(`Worker job ${id} (${fnName}) started`); - } - this._jobs[id] = { - fnName, - start: Date.now(), - }; - - return id; - } - - private _updateJob(id: number, data: any) { - const { resolve, reject } = data; - const { fnName, start } = this._jobs[id]; - - this._jobs[id] = { - ...this._jobs[id], - ...data, - resolve: (value: any) => { - this._removeJob(id); - const end = Date.now(); - if (this._DEBUG) { - window.log.info(`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`); - } - return resolve(value); - }, - reject: (error: any) => { - this._removeJob(id); - const end = Date.now(); - window.log.info(`Worker job ${id} (${fnName}) failed in ${end - start}ms`); - return reject(error); - }, - }; - } - - private _removeJob(id: number) { - if (this._DEBUG) { - this._jobs[id].complete = true; - } else { - // tslint:disable-next-line: no-dynamic-delete - delete this._jobs[id]; - } - } - - private _getJob(id: number) { - return this._jobs[id]; - } -} diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index b1a94427a..29b3151fc 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -12,6 +12,7 @@ import { OpenGroupRequestCommonType } from '../session/apis/open_group_api/openg import { FSv2 } from '../session/apis/file_server_api'; import { getUnpaddedAttachment } from '../session/crypto/BufferPadding'; import { decryptAttachment } from '../util/crypto/attachmentsEncrypter'; +import { callUtilsWorker } from '../webworker/workers/util_worker_interface'; export async function downloadAttachment(attachment: { url: string; @@ -61,11 +62,8 @@ export async function downloadAttachment(attachment: { throw new Error('Attachment expected size is 0'); } - const keyBuffer = (await window.callWorker('fromBase64ToArrayBuffer', key)) as ArrayBuffer; - const digestBuffer = (await window.callWorker( - 'fromBase64ToArrayBuffer', - digest - )) as ArrayBuffer; + const keyBuffer = (await callUtilsWorker('fromBase64ToArrayBuffer', key)) as ArrayBuffer; + const digestBuffer = (await callUtilsWorker('fromBase64ToArrayBuffer', digest)) as ArrayBuffer; data = await decryptAttachment(data, keyBuffer, digestBuffer); diff --git a/ts/session/apis/file_server_api/FileServerApiV2.ts b/ts/session/apis/file_server_api/FileServerApiV2.ts index 6bcdde631..3496d20ea 100644 --- a/ts/session/apis/file_server_api/FileServerApiV2.ts +++ b/ts/session/apis/file_server_api/FileServerApiV2.ts @@ -1,3 +1,4 @@ +import { callUtilsWorker } from '../../../webworker/workers/util_worker_interface'; import { OpenGroupV2Request } from '../open_group_api/opengroupV2/ApiUtil'; import { sendApiV2Request } from '../open_group_api/opengroupV2/OpenGroupAPIV2'; import { parseStatusCodeFromOnionRequest } from '../open_group_api/opengroupV2/OpenGroupAPIV2Parser'; @@ -35,7 +36,7 @@ export const uploadFileToFsV2 = async ( return null; } const queryParams = { - file: await window.callWorker('arrayBufferToStringBase64', fileContent), + file: await callUtilsWorker('arrayBufferToStringBase64', fileContent), }; const request: FileServerV2Request = { @@ -99,7 +100,7 @@ export const downloadFileFromFSv2 = async ( if (!base64Data) { return null; } - return window.callWorker('fromBase64ToArrayBuffer', base64Data); + return callUtilsWorker('fromBase64ToArrayBuffer', base64Data); }; /** diff --git a/ts/session/apis/open_group_api/opengroupV2/ApiAuth.ts b/ts/session/apis/open_group_api/opengroupV2/ApiAuth.ts index 86a17a2f4..cd483b4dd 100644 --- a/ts/session/apis/open_group_api/opengroupV2/ApiAuth.ts +++ b/ts/session/apis/open_group_api/opengroupV2/ApiAuth.ts @@ -1,4 +1,5 @@ import { getV2OpenGroupRoomByRoomId, saveV2OpenGroupRoom } from '../../../../data/opengroups'; +import { callUtilsWorker } from '../../../../webworker/workers/util_worker_interface'; import { allowOnlyOneAtATime } from '../../../utils/Promise'; import { toHex } from '../../../utils/String'; import { getIdentityKeyPair, getOurPubKeyStrFromCache } from '../../../utils/User'; @@ -120,22 +121,22 @@ export async function requestNewAuthToken({ window?.log?.warn('Parsing failed'); return null; } - const ciphertext = (await window.callWorker( + const ciphertext = (await callUtilsWorker( 'fromBase64ToArrayBuffer', base64EncodedCiphertext )) as ArrayBuffer; - const ephemeralPublicKey = (await window.callWorker( + const ephemeralPublicKey = (await callUtilsWorker( 'fromBase64ToArrayBuffer', base64EncodedEphemeralPublicKey )) as ArrayBuffer; try { - const symmetricKey = (await window.callWorker( + const symmetricKey = (await callUtilsWorker( 'deriveSymmetricKey', new Uint8Array(ephemeralPublicKey), new Uint8Array(userKeyPair.privKey) )) as ArrayBuffer; - const plaintextBuffer = await window.callWorker( + const plaintextBuffer = await callUtilsWorker( 'DecryptAESGCM', new Uint8Array(symmetricKey), new Uint8Array(ciphertext) diff --git a/ts/session/apis/open_group_api/opengroupV2/ApiUtil.ts b/ts/session/apis/open_group_api/opengroupV2/ApiUtil.ts index daa3a565e..786377746 100644 --- a/ts/session/apis/open_group_api/opengroupV2/ApiUtil.ts +++ b/ts/session/apis/open_group_api/opengroupV2/ApiUtil.ts @@ -11,6 +11,7 @@ import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils'; import { parseOpenGroupV2 } from './JoinOpenGroupV2'; import { getAllRoomInfos } from './OpenGroupAPIV2'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; +import { callUtilsWorker } from '../../../../webworker/workers/util_worker_interface'; export type OpenGroupRequestCommonType = { serverUrl: string; @@ -44,15 +45,15 @@ export type OpenGroupV2InfoJoinable = OpenGroupV2Info & { }; export const TextToBase64 = async (text: string) => { - const arrayBuffer = await window.callWorker('bytesFromString', text); + const arrayBuffer = await callUtilsWorker('bytesFromString', text); - const base64 = await window.callWorker('arrayBufferToStringBase64', arrayBuffer); + const base64 = await callUtilsWorker('arrayBufferToStringBase64', arrayBuffer); return base64; }; export const textToArrayBuffer = async (text: string) => { - return window.callWorker('bytesFromString', text); + return callUtilsWorker('bytesFromString', text); }; export const verifyED25519Signature = async ( @@ -60,7 +61,7 @@ export const verifyED25519Signature = async ( base64EncodedData: string, base64EncondedSignature: string ): Promise => { - return window.callWorker('verifySignature', pubkey, base64EncodedData, base64EncondedSignature); + return callUtilsWorker('verifySignature', pubkey, base64EncodedData, base64EncondedSignature); }; export const parseMessages = async ( @@ -88,7 +89,7 @@ export const parseMessages = async ( // Validate the message signature const senderPubKey = PubKey.cast(opengroupv2Message.sender).withoutPrefix(); - const signatureValid = (await window.callWorker( + const signatureValid = (await callUtilsWorker( 'verifySignature', senderPubKey, opengroupv2Message.base64EncodedData, diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupAPIV2.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupAPIV2.ts index 773c53160..a9b6bb610 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupAPIV2.ts @@ -17,6 +17,7 @@ import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; import { isOpenGroupV2Request } from '../../file_server_api/FileServerApiV2'; import { getAuthToken } from './ApiAuth'; import pRetry from 'p-retry'; +import { callUtilsWorker } from '../../../../webworker/workers/util_worker_interface'; // used to be overwritten by testing export const getMinTimeout = () => 1000; @@ -399,7 +400,7 @@ export const downloadFileOpenGroupV2 = async ( if (!base64Data) { return null; } - return new Uint8Array(await window.callWorker('fromBase64ToArrayBuffer', base64Data)); + return new Uint8Array(await callUtilsWorker('fromBase64ToArrayBuffer', base64Data)); }; export const downloadFileOpenGroupV2ByUrl = async ( @@ -426,7 +427,7 @@ export const downloadFileOpenGroupV2ByUrl = async ( if (!base64Data) { return null; } - return new Uint8Array(await window.callWorker('fromBase64ToArrayBuffer', base64Data)); + return new Uint8Array(await callUtilsWorker('fromBase64ToArrayBuffer', base64Data)); }; /** @@ -472,7 +473,7 @@ export const uploadFileOpenGroupV2 = async ( return null; } const queryParams = { - file: await window.callWorker('arrayBufferToStringBase64', fileContent), + file: await callUtilsWorker('arrayBufferToStringBase64', fileContent), }; const filesEndpoint = 'files'; @@ -512,7 +513,7 @@ export const uploadImageForRoomOpenGroupV2 = async ( } const queryParams = { - file: await window.callWorker('arrayBufferToStringBase64', fileContent), + file: await callUtilsWorker('arrayBufferToStringBase64', fileContent), }; const imageEndpoint = `rooms/${roomInfos.roomId}/image`; diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupMessageV2.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupMessageV2.ts index 5d36b3dba..cf75b5be9 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupMessageV2.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupMessageV2.ts @@ -1,3 +1,4 @@ +import { callUtilsWorker } from '../../../../webworker/workers/util_worker_interface'; import { UserUtils } from '../../../utils'; import { fromBase64ToArray } from '../../../utils/String'; @@ -66,7 +67,7 @@ export class OpenGroupMessageV2 { if (!signature || signature.length === 0) { throw new Error("Couldn't sign message"); } - const base64Sig = await window.callWorker('arrayBufferToStringBase64', signature); + const base64Sig = await callUtilsWorker('arrayBufferToStringBase64', signature); return new OpenGroupMessageV2({ base64EncodedData: this.base64EncodedData, sentTimestamp: this.sentTimestamp, diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts index 14e438d33..a975918fe 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts @@ -22,6 +22,7 @@ import { DURATION } from '../../../constants'; import { processNewAttachment } from '../../../../types/MessageAttachment'; import { MIME } from '../../../../types'; import { handleOpenGroupV2Message } from '../../../../receiver/opengroup'; +import { callUtilsWorker } from '../../../../webworker/workers/util_worker_interface'; const pollForEverythingInterval = DURATION.SECONDS * 10; const pollForRoomAvatarInterval = DURATION.DAYS * 1; @@ -493,7 +494,7 @@ const handleBase64AvatarUpdate = async ( const upgradedAttachment = await processNewAttachment({ isRaw: true, - data: await window.callWorker('fromBase64ToArrayBuffer', res.base64), + data: await callUtilsWorker('fromBase64ToArrayBuffer', res.base64), contentType: MIME.IMAGE_UNKNOWN, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case. // url: `${serverUrl}/${res.roomId}`, }); // update the hash on the conversationModel diff --git a/ts/session/apis/push_notification_api/PnServer.ts b/ts/session/apis/push_notification_api/PnServer.ts index d147563cc..c169c6fb5 100644 --- a/ts/session/apis/push_notification_api/PnServer.ts +++ b/ts/session/apis/push_notification_api/PnServer.ts @@ -1,3 +1,4 @@ +import { callUtilsWorker } from '../../../webworker/workers/util_worker_interface'; import { sendViaOnionToNonSnode } from '../../onions/onionSend'; const pnServerPubkeyHex = '642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049'; @@ -11,7 +12,7 @@ export async function notifyPnServer(wrappedEnvelope: ArrayBuffer, sentTo: strin const options: ServerRequestOptionsType = { method: 'post', objBody: { - data: await window.callWorker('arrayBufferToStringBase64', wrappedEnvelope), + data: await callUtilsWorker('arrayBufferToStringBase64', wrappedEnvelope), send_to: sentTo, }, }; diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts index cf863e133..f83bcb94e 100644 --- a/ts/session/apis/snode_api/onions.ts +++ b/ts/session/apis/snode_api/onions.ts @@ -15,6 +15,7 @@ import { Snode } from '../../../data/data'; import { ERROR_CODE_NO_CONNECT } from './SNodeAPI'; import { Onions } from '.'; import { hrefPnServerDev, hrefPnServerProd } from '../push_notification_api/PnServer'; +import { callUtilsWorker } from '../../../webworker/workers/util_worker_interface'; export const resetSnodeFailureCount = () => { snodeFailureCount = {}; @@ -51,7 +52,9 @@ async function encryptForPubKey(pubKeyX25519hex: string, reqObj: any): Promise; } export type DestinationRelayV2 = { @@ -79,7 +82,7 @@ async function encryptForRelayV2( }; const plaintext = encodeCiphertextPlusJson(ctx.ciphertext, reqObj); - return window.callWorker('encryptForPubkey', relayX25519hex, plaintext); + return callUtilsWorker('encryptForPubkey', relayX25519hex, plaintext); } /// Encode ciphertext as (len || binary) and append payloadJson as utf8 @@ -389,13 +392,13 @@ export async function decodeOnionResult(symmetricKey: ArrayBuffer, ciphertext: s } catch (e) { // just try to get a json object from what is inside (for PN requests), if it fails, continue () } - const ciphertextBuffer = await window.callWorker('fromBase64ToArrayBuffer', parsedCiphertext); + const ciphertextBuffer = await callUtilsWorker('fromBase64ToArrayBuffer', parsedCiphertext); - const plaintextBuffer = await window.callWorker( + const plaintextBuffer = (await callUtilsWorker( 'DecryptAESGCM', new Uint8Array(symmetricKey), new Uint8Array(ciphertextBuffer) - ); + )) as string; return { plaintext: new TextDecoder().decode(plaintextBuffer), ciphertextBuffer }; } @@ -743,7 +746,11 @@ const sendOnionRequest = async ({ const bodyEncoded = textEncoder.encode(body); const plaintext = encodeCiphertextPlusJson(bodyEncoded, options); - destCtx = await window.callWorker('encryptForPubkey', destX25519hex, plaintext); + destCtx = (await callUtilsWorker( + 'encryptForPubkey', + destX25519hex, + plaintext + )) as DestinationContext; } else { destCtx = await encryptForPubKey(destX25519hex, options); } diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts index 4b69d1d67..d4f6072a0 100644 --- a/ts/session/crypto/DecryptedAttachmentsManager.ts +++ b/ts/session/crypto/DecryptedAttachmentsManager.ts @@ -10,7 +10,7 @@ import * as fse from 'fs-extra'; import { DURATION } from '../constants'; import { makeObjectUrl, urlToBlob } from '../../types/attachments/VisualAttachment'; import { getAttachmentPath } from '../../types/MessageAttachment'; -import { decryptAttachmentBuffer } from '../../node/local_attachments_encrypter'; +import { decryptAttachmentBuffer } from '../../util/local_attachments_encrypter'; const urlToDecryptedBlobMap = new Map< string, diff --git a/ts/node/local_attachments_encrypter.ts b/ts/util/local_attachments_encrypter.ts similarity index 63% rename from ts/node/local_attachments_encrypter.ts rename to ts/util/local_attachments_encrypter.ts index 8f1511d75..7a5b0f6a3 100644 --- a/ts/node/local_attachments_encrypter.ts +++ b/ts/util/local_attachments_encrypter.ts @@ -1,31 +1,32 @@ import { isArrayBuffer } from 'lodash'; import { fromHexToArray } from '../session/utils/String'; -import { sqlNode } from './sql'; +// import { callUtilsWorker } from '../webworker/workers/util_worker_interface'; +import { getItemById } from '../data/channelsItem'; export const encryptAttachmentBuffer = async (bufferIn: ArrayBuffer) => { if (!isArrayBuffer(bufferIn)) { throw new TypeError("'bufferIn' must be an array buffer"); } - const key = sqlNode.getItemById('local_attachment_encrypted_key')?.value as string | undefined; + const key = (await getItemById('local_attachment_encrypted_key'))?.value as string | undefined; if (!key) { throw new TypeError( "'encryptAttachmentBuffer' needs a key set in local_attachment_encrypted_key" ); } const encryptingKey = fromHexToArray(key); - return window.callWorker('encryptAttachmentBuffer', encryptingKey, bufferIn); + return callUtilsWorker('encryptAttachmentBuffer', encryptingKey, bufferIn); }; export const decryptAttachmentBuffer = async (bufferIn: ArrayBuffer): Promise => { if (!isArrayBuffer(bufferIn)) { throw new TypeError("'bufferIn' must be an array buffer"); } - const key = sqlNode.getItemById('local_attachment_encrypted_key')?.value as string; + const key = (await getItemById('local_attachment_encrypted_key'))?.value as string; if (!key) { throw new TypeError( "'decryptAttachmentBuffer' needs a key set in local_attachment_encrypted_key" ); } const encryptingKey = fromHexToArray(key); - return window.callWorker('decryptAttachmentBuffer', encryptingKey, bufferIn); + return callUtilsWorker('decryptAttachmentBuffer', encryptingKey, bufferIn); }; diff --git a/ts/webworker/master.ts b/ts/webworker/master.ts deleted file mode 100644 index 3c36d1518..000000000 --- a/ts/webworker/master.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function yo() { - const worker = new Worker('./ts/webworker/workers/auth.worker.js', { type: 'module' }); - worker.postMessage({ - question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.', - }); - worker.onmessage = ({ data: { answer } }) => { - console.log(`The Answer to the Ultimate`, answer); - }; - // const hashed = await auth.hashPassword('Super secret password', '1234'); - - // console.log('Hashed password:', hashed); -} diff --git a/ts/webworker/worker_interface.ts b/ts/webworker/worker_interface.ts new file mode 100644 index 000000000..9ed0293c1 --- /dev/null +++ b/ts/webworker/worker_interface.ts @@ -0,0 +1,120 @@ +const WORKER_TIMEOUT = 60 * 1000; // one minute + +class TimedOutError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error(message).stack; + } + } +} + +export class WorkerInterface { + private readonly timeout: number; + private readonly _DEBUG: boolean; + private _jobCounter: number; + private readonly _jobs: Record; + private readonly _utilWorker: Worker; + + constructor(path: string, timeout = WORKER_TIMEOUT) { + this._utilWorker = new Worker(path); + this.timeout = timeout; + this._jobs = Object.create(null); + this._DEBUG = false; + this._jobCounter = 0; + + this._utilWorker.onmessage = e => { + const [jobId, errorForDisplay, result] = e.data; + + const job = this._getJob(jobId); + if (!job) { + throw new Error( + `Received worker reply to job ${jobId}, but did not have it in our registry!` + ); + } + + const { resolve, reject, fnName } = job; + + if (errorForDisplay) { + return reject( + new Error(`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`) + ); + } + + return resolve(result); + }; + } + + public async callWorker(fnName: string, ...args: any) { + const jobId = this._makeJob(fnName); + + return new Promise((resolve, reject) => { + this._utilWorker.postMessage([jobId, fnName, ...args]); + + this._updateJob(jobId, { + resolve, + reject, + args: this._DEBUG ? args : null, + }); + + setTimeout(() => { + reject(new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)); + }, this.timeout); + }); + } + + private _makeJob(fnName: string): number { + this._jobCounter += 1; + const id = this._jobCounter; + + if (this._DEBUG) { + window.log.info(`Worker job ${id} (${fnName}) started`); + } + this._jobs[id] = { + fnName, + start: Date.now(), + }; + + return id; + } + + private _updateJob(id: number, data: any) { + const { resolve, reject } = data; + const { fnName, start } = this._jobs[id]; + + this._jobs[id] = { + ...this._jobs[id], + ...data, + resolve: (value: any) => { + this._removeJob(id); + const end = Date.now(); + if (this._DEBUG) { + window.log.info(`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`); + } + return resolve(value); + }, + reject: (error: any) => { + this._removeJob(id); + const end = Date.now(); + window.log.info(`Worker job ${id} (${fnName}) failed in ${end - start}ms`); + return reject(error); + }, + }; + } + + private _removeJob(id: number) { + if (this._DEBUG) { + this._jobs[id].complete = true; + } else { + // tslint:disable-next-line: no-dynamic-delete + delete this._jobs[id]; + } + } + + private _getJob(id: number) { + return this._jobs[id]; + } +} diff --git a/ts/webworker/workers/auth.worker.ts b/ts/webworker/workers/auth.worker.ts deleted file mode 100644 index 263064f7a..000000000 --- a/ts/webworker/workers/auth.worker.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as lodash from 'lodash'; -// import * as _ from 'lodash'; - -const sleep = async (time: any) => new Promise(r => setTimeout(r, time)); - -~(async function main() { - while (true) { - console.log('lodash map exists:', typeof lodash.map); - await sleep(1000); - } -})(); - -self.onmessage = ({ data: { question } }) => { - self.postMessage({ - answer: `PLOP + ${question} + PLOP + 42`, - }); -}; diff --git a/ts/webworker/workers/index.ts b/ts/webworker/workers/index.ts new file mode 100644 index 000000000..e5c972ea7 --- /dev/null +++ b/ts/webworker/workers/index.ts @@ -0,0 +1,2 @@ +import * as utilWorkerInterface from './util_worker_interface'; +export { utilWorkerInterface }; diff --git a/js/util_worker_tasks.js b/ts/webworker/workers/util.worker.ts similarity index 72% rename from js/util_worker_tasks.js rename to ts/webworker/workers/util.worker.ts index bf6f98649..5ee6f14a5 100644 --- a/js/util_worker_tasks.js +++ b/ts/webworker/workers/util.worker.ts @@ -1,4 +1,69 @@ -/* global dcodeIO, Internal, libsignal, sodium */ +import ByteBuffer from 'bytebuffer'; +import { generateKeyPair, sharedKey, verify } from 'curve25519-js'; +import { default as sodiumWrappers } from 'libsodium-wrappers-sumo'; + +async function getSodium() { + await sodiumWrappers.ready; + return sodiumWrappers; +} + +export async function decryptAttachmentBuffer(encryptingKey: Uint8Array, bufferIn: ArrayBuffer) { + const sodium = await getSodium(); + + const header = new Uint8Array( + bufferIn.slice(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES) + ); + + const encryptedBuffer = new Uint8Array( + bufferIn.slice(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES) + ); + try { + /* Decrypt the stream: initializes the state, using the key and a header */ + const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, encryptingKey); + // what if ^ this call fail (? try to load as a unencrypted attachment?) + + const messageTag = sodium.crypto_secretstream_xchacha20poly1305_pull(state, encryptedBuffer); + // we expect the final tag to be there. If not, we might have an issue with this file + // maybe not encrypted locally? + if (messageTag.tag === sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) { + return messageTag.message; + } + } catch (e) { + // tslint:disable: no-console + console.error('Failed to load the file as an encrypted one', e); + } + return new Uint8Array(); +} + +export async function encryptAttachmentBuffer(encryptingKey: Uint8Array, bufferIn: ArrayBuffer) { + const sodium = await getSodium(); + + try { + const uintArrayIn = new Uint8Array(bufferIn); + + /* Set up a new stream: initialize the state and create the header */ + const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptingKey); + /* Now, encrypt the buffer. */ + const bufferOut = sodium.crypto_secretstream_xchacha20poly1305_push( + state, + uintArrayIn, + null, + sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL + ); + + const encryptedBufferWithHeader = new Uint8Array(bufferOut.length + header.length); + encryptedBufferWithHeader.set(header); + encryptedBufferWithHeader.set(bufferOut, header.length); + + return { encryptedBufferWithHeader, header }; + } catch (e) { + console.error('encryptAttachmentBuffer error: ', e); + + return null; + } +} + +/* global dcodeIO, Internal, libsignal */ /* eslint-disable no-console */ /* eslint-disable strict */ @@ -10,17 +75,17 @@ const functions = { DecryptAESGCM, deriveSymmetricKey, encryptForPubkey, - generateEphemeralKeyPair, decryptAttachmentBuffer, encryptAttachmentBuffer, bytesFromString, }; +// tslint:disable: function-name onmessage = async e => { const [jobId, fnName, ...args] = e.data; try { - const fn = functions[fnName]; + const fn = (functions as any)[fnName]; if (!fn) { throw new Error(`Worker: job ${jobId} did not find function ${fnName}`); } @@ -32,7 +97,7 @@ onmessage = async e => { } }; -function prepareErrorForPostMessage(error) { +function prepareErrorForPostMessage(error: any) { if (!error) { return null; } @@ -44,28 +109,32 @@ function prepareErrorForPostMessage(error) { return error.message; } -function arrayBufferToStringBase64(arrayBuffer) { - return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); +function arrayBufferToStringBase64(arrayBuffer: ArrayBuffer) { + return ByteBuffer.wrap(arrayBuffer).toString('base64'); } -function fromBase64ToArrayBuffer(base64Str) { - return dcodeIO.ByteBuffer.wrap(base64Str, 'base64').toArrayBuffer(); +function fromBase64ToArrayBuffer(base64Str: string) { + return ByteBuffer.wrap(base64Str, 'base64').toArrayBuffer(); } -function fromHexToArray(hexStr) { - return new Uint8Array(dcodeIO.ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer()); +function fromHexToArray(hexStr: string) { + return new Uint8Array(ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer()); } -function fromHexToArrayBuffer(hexStr) { - return dcodeIO.ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer(); +function fromHexToArrayBuffer(hexStr: string) { + return ByteBuffer.wrap(hexStr, 'hex').toArrayBuffer(); } -function bytesFromString(string) { - return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer(); +function bytesFromString(str: string) { + return ByteBuffer.wrap(str, 'utf8').toArrayBuffer(); } // hexString, base64String, base64String -async function verifySignature(senderPubKey, messageBase64, signatureBase64) { +async function verifySignature( + senderPubKey: string, + messageBase64: string, + signatureBase64: string +) { try { if (typeof senderPubKey !== 'string') { throw new Error('senderPubKey type not correct'); @@ -78,14 +147,11 @@ async function verifySignature(senderPubKey, messageBase64, signatureBase64) { } const messageData = new Uint8Array(fromBase64ToArrayBuffer(messageBase64)); const signature = new Uint8Array(fromBase64ToArrayBuffer(signatureBase64)); - // verify returns true if the signature is not correct - const verifyRet = Internal.curve25519.verify( - fromHexToArray(senderPubKey), - messageData, - signature - ); - if (verifyRet) { + + const verifyRet = verify(fromHexToArray(senderPubKey), messageData, signature); + + if (!verifyRet) { console.error('Invalid signature'); return false; } @@ -99,14 +165,10 @@ async function verifySignature(senderPubKey, messageBase64, signatureBase64) { const NONCE_LENGTH = 12; -// uint8array, uint8array -async function deriveSymmetricKey(x25519PublicKey, x25519PrivateKey) { +async function deriveSymmetricKey(x25519PublicKey: Uint8Array, x25519PrivateKey: Uint8Array) { assertArrayBufferView(x25519PublicKey); assertArrayBufferView(x25519PrivateKey); - const ephemeralSecret = await libsignal.Curve.async.calculateAgreement( - x25519PublicKey.buffer, - x25519PrivateKey.buffer - ); + const ephemeralSecret = sharedKey(x25519PrivateKey, x25519PublicKey); const salt = bytesFromString('LOKI'); @@ -127,20 +189,22 @@ async function deriveSymmetricKey(x25519PublicKey, x25519PrivateKey) { } async function generateEphemeralKeyPair() { - const keys = await libsignal.Curve.async.generateKeyPair(); - // Signal protocol prepends with "0x05" - keys.pubKey = keys.pubKey.slice(1); + const ran = (await getSodium()).randombytes_buf(32); + const keys = generateKeyPair(ran); return keys; + // Signal protocol prepends with "0x05" + // keys.pubKey = keys.pubKey.slice(1); + // return { pubKey: keys.public, privKey: keys.private }; } -function assertArrayBufferView(val) { +function assertArrayBufferView(val: any) { if (!ArrayBuffer.isView(val)) { throw new Error('val type not correct'); } } // encryptForPubkey: hexString, payloadBytes: Uint8Array -async function encryptForPubkey(pubkeyX25519str, payloadBytes) { +async function encryptForPubkey(pubkeyX25519str: string, payloadBytes: Uint8Array) { try { if (typeof pubkeyX25519str !== 'string') { throw new Error('pubkeyX25519str type not correct'); @@ -150,18 +214,18 @@ async function encryptForPubkey(pubkeyX25519str, payloadBytes) { const pubkeyX25519Buffer = fromHexToArray(pubkeyX25519str); const symmetricKey = await deriveSymmetricKey( pubkeyX25519Buffer, - new Uint8Array(ephemeral.privKey) + new Uint8Array(ephemeral.private) ); const ciphertext = await EncryptAESGCM(symmetricKey, payloadBytes); - return { ciphertext, symmetricKey, ephemeralKey: ephemeral.pubKey }; + return { ciphertext, symmetricKey, ephemeralKey: ephemeral.public }; } catch (e) { console.error('encryptForPubkey got an error:', e); return null; } } -async function EncryptAESGCM(symmetricKey, plaintext) { +async function EncryptAESGCM(symmetricKey: ArrayBuffer, plaintext: ArrayBuffer) { const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); const key = await crypto.subtle.importKey('raw', symmetricKey, { name: 'AES-GCM' }, false, [ @@ -174,6 +238,7 @@ async function EncryptAESGCM(symmetricKey, plaintext) { plaintext ); + // tslint:disable-next-line: restrict-plus-operands const ivAndCiphertext = new Uint8Array(NONCE_LENGTH + ciphertext.byteLength); ivAndCiphertext.set(nonce); @@ -183,7 +248,7 @@ async function EncryptAESGCM(symmetricKey, plaintext) { } // uint8array, uint8array -async function DecryptAESGCM(symmetricKey, ivAndCiphertext) { +async function DecryptAESGCM(symmetricKey: Uint8Array, ivAndCiphertext: Uint8Array) { assertArrayBufferView(symmetricKey); assertArrayBufferView(ivAndCiphertext); @@ -200,65 +265,3 @@ async function DecryptAESGCM(symmetricKey, ivAndCiphertext) { return crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, key, ciphertext); } - -async function getSodium() { - await sodium.ready; - return sodium; -} - -// Uint8Array, ArrayBuffer -async function decryptAttachmentBuffer(encryptingKey, bufferIn) { - const sodium = await getSodium(); - - const header = new Uint8Array( - bufferIn.slice(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES) - ); - - const encryptedBuffer = new Uint8Array( - bufferIn.slice(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES) - ); - try { - /* Decrypt the stream: initializes the state, using the key and a header */ - const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, encryptingKey); - // what if ^ this call fail (? try to load as a unencrypted attachment?) - - const messageTag = sodium.crypto_secretstream_xchacha20poly1305_pull(state, encryptedBuffer); - // we expect the final tag to be there. If not, we might have an issue with this file - // maybe not encrypted locally? - if (messageTag.tag === sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) { - return messageTag.message; - } - } catch (e) { - console.error('Failed to load the file as an encrypted one', e); - } - return new Uint8Array(); -} - -// Uint8Array, ArrayBuffer -async function encryptAttachmentBuffer(encryptingKey, bufferIn) { - const sodium = await getSodium(); - - try { - const uintArrayIn = new Uint8Array(bufferIn); - - /* Set up a new stream: initialize the state and create the header */ - const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptingKey); - /* Now, encrypt the buffer. */ - const bufferOut = sodium.crypto_secretstream_xchacha20poly1305_push( - state, - uintArrayIn, - null, - sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL - ); - - const encryptedBufferWithHeader = new Uint8Array(bufferOut.length + header.length); - encryptedBufferWithHeader.set(header); - encryptedBufferWithHeader.set(bufferOut, header.length); - - return { encryptedBufferWithHeader, header }; - } catch (e) { - console.error('encryptAttachmentBuffer error: ', e); - - return null; - } -} diff --git a/ts/webworker/workers/util_worker_interface.ts b/ts/webworker/workers/util_worker_interface.ts new file mode 100644 index 000000000..eb2fae0bb --- /dev/null +++ b/ts/webworker/workers/util_worker_interface.ts @@ -0,0 +1,9 @@ +import { WorkerInterface } from '../worker_interface'; +import { join } from 'path'; + +const utilWorkerPath = join('./', 'ts', 'webworker', 'workers', 'util.worker.js'); //app.getAppPath() +const utilWorkerInterface = new WorkerInterface(utilWorkerPath, 3 * 60 * 1000); //{ type: 'module' } + +export const callUtilsWorker = async (fnName: string, ...args: any): Promise => { + return utilWorkerInterface.callWorker(fnName, ...args); +}; diff --git a/ts/window.d.ts b/ts/window.d.ts index 1e280ed57..c972b8e6e 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -91,7 +91,6 @@ declare global { LokiPushNotificationServer: any; getGlobalOnlineStatus: () => boolean; confirmationDialog: any; - callWorker: (fnName: string, ...args: any) => Promise; setStartInTray: (val: boolean) => Promise; getStartInTray: () => Promise; closeAbout: () => void;