1 import type { Remote } from 'comlink';
2 import { releaseProxy, transferHandlers, wrap } from 'comlink';
4 import { getIsNetworkError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
5 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
7 import type { Api as CryptoApi, ApiInterface as CryptoApiInterface } from './api';
8 import { mainThreadTransferHandlers } from './transferHandlers';
10 export interface WorkerInitOptions {
14 export interface WorkerPoolInitOptions {
16 openpgpConfigOptions?: WorkerInitOptions;
19 export interface WorkerPoolInterface extends CryptoApiInterface {
21 * Setup worker pool (singleton instance):
22 * create and start workers, and initializes internal Crypto API (incl. pmcrypto and OpenPGP.js)
23 * @param options.poolSize - number of workers to start; defaults to `Navigator.hardwareConcurrency()` if available, otherwise to 1.
25 init(options?: WorkerPoolInitOptions): Promise<void>;
28 * Close all workers, after clearing their internal key store.
29 * After the pool has been destroyed, it is possible to `init()` it again.
31 destroy(): Promise<void>;
34 const errorReporter = (err: Error) => {
35 if (getIsNetworkError(err)) {
36 captureMessage('Network error in crypto worker', {
38 extra: { message: err.message },
45 const reportKeyCompatibilityErrorIfPresent = (compatibilityError: Error | null) => {
47 captureMessage('Key compatibility error', {
49 extra: { message: compatibilityError.message, stack: compatibilityError.stack },
53 // Singleton worker pool.
54 export const CryptoWorkerPool: WorkerPoolInterface = (() => {
55 let workerPool: Remote<CryptoApi>[] | null = null;
58 const initWorker = async (openpgpConfigOptions: WorkerInitOptions) => {
59 // Webpack static analyser is not especially powerful at detecting web workers that require bundling,
60 // see: https://github.com/webpack/webpack.js.org/issues/4898#issuecomment-823073304.
61 // Harcoding the path here is the easiet way to get the worker to be bundled properly.
62 const RemoteApi = wrap<typeof CryptoApi>(
63 openpgpConfigOptions.v6Canary
66 /* webpackChunkName: "crypto-worker-v6-canary" */
67 './worker_v6_canary.ts',
73 /* webpackChunkName: "crypto-worker" */
80 const worker = await new RemoteApi();
84 const destroyWorker = async (worker: Remote<CryptoApi>) => {
85 await worker?.clearKeyStore();
86 worker?.[releaseProxy]();
90 * Get worker from the pool pool. By default, the workers are picked in a round-robin fashion, to balance the load.
91 * However, this might not be desirable for operations like e.g. argon2, which is resource intensive and caches them
92 * (wasm module & allocated memory) across calls.
93 * @param [fixed] - whether to always return the same worker
95 const getWorker = (fixed = false): Remote<CryptoApi> => {
96 if (workerPool == null) {
97 throw new Error('Uninitialised worker pool');
100 return workerPool[0];
102 i = (i + 1) % workerPool.length;
103 return workerPool[i];
106 // The return type is technically `Remote<CryptoApi>[]` but that removes some type inference capabilities that are
107 // useful to type-check the internal worker pool functions.
108 const getAllWorkers = (): CryptoApi[] => {
109 if (workerPool == null) {
110 throw new Error('Uninitialised worker pool');
112 return workerPool as any as CryptoApi[];
116 init: async ({ poolSize = navigator.hardwareConcurrency || 1, openpgpConfigOptions = {} } = {}) => {
117 if (workerPool !== null) {
118 throw new Error('worker pool already initialised');
120 // We load one worker early to ensure the browser serves the cached resources to the rest of the pool
121 workerPool = [await initWorker(openpgpConfigOptions)];
123 workerPool = workerPool.concat(
124 await Promise.all(new Array(poolSize - 1).fill(null).map(() => initWorker(openpgpConfigOptions)))
127 mainThreadTransferHandlers.forEach(({ name, handler }) => transferHandlers.set(name, handler));
129 destroy: async () => {
130 workerPool && (await Promise.all(workerPool.map(destroyWorker)));
133 // @ts-ignore marked as non-callable, unclear why, might be due to a limitation of type Remote
134 encryptMessage: (opts) => getWorker().encryptMessage(opts).catch(errorReporter),
135 decryptMessage: (opts) => getWorker().decryptMessage(opts).catch(errorReporter),
136 // @ts-ignore marked as non-callable, unclear why, might be due to a limitation of type Remote
137 signMessage: (opts) => getWorker().signMessage(opts).catch(errorReporter),
138 // @ts-ignore marked as non-callable, unclear why, might be due to a limitation of type Remote
139 verifyMessage: (opts) => getWorker().verifyMessage(opts),
140 verifyCleartextMessage: (opts) => getWorker().verifyCleartextMessage(opts).catch(errorReporter),
141 processMIME: (opts) => getWorker().processMIME(opts).catch(errorReporter),
142 computeHash: (opts) => getWorker().computeHash(opts).catch(errorReporter),
143 computeHashStream: (opts) => getWorker().computeHashStream(opts).catch(errorReporter),
144 computeArgon2: (opts) => getWorker(true).computeArgon2(opts).catch(errorReporter),
146 generateSessionKey: (opts) => getWorker().generateSessionKey(opts).catch(errorReporter),
147 generateSessionKeyForAlgorithm: (opts) => getWorker().generateSessionKeyForAlgorithm(opts).catch(errorReporter),
148 encryptSessionKey: (opts) => getWorker().encryptSessionKey(opts).catch(errorReporter),
149 decryptSessionKey: (opts) => getWorker().decryptSessionKey(opts).catch(errorReporter),
150 importPrivateKey: async (opts) => {
151 const [first, ...rest] = getAllWorkers();
152 const result = await first.importPrivateKey(opts).catch(errorReporter);
153 reportKeyCompatibilityErrorIfPresent(result._getCompatibilityError());
154 await Promise.all(rest.map((worker) => worker.importPrivateKey(opts, result._idx)));
157 importPublicKey: async (opts) => {
158 const [first, ...rest] = getAllWorkers();
159 const result = await first.importPublicKey(opts).catch(errorReporter);
160 reportKeyCompatibilityErrorIfPresent(result._getCompatibilityError());
161 await Promise.all(rest.map((worker) => worker.importPublicKey(opts, result._idx)));
164 generateKey: async (opts) => {
165 const [first, ...rest] = getAllWorkers();
166 const keyReference = await first.generateKey(opts).catch(errorReporter);
167 const key = await first.exportPrivateKey({ privateKey: keyReference, passphrase: null, format: 'binary' });
169 rest.map((worker) => worker.importPrivateKey({ binaryKey: key, passphrase: null }, keyReference._idx))
173 reformatKey: async (opts) => {
174 const [first, ...rest] = getAllWorkers();
175 const keyReference = await first.reformatKey(opts).catch(errorReporter);
176 const key = await first.exportPrivateKey({ privateKey: keyReference, passphrase: null, format: 'binary' });
178 rest.map((worker) => worker.importPrivateKey({ binaryKey: key, passphrase: null }, keyReference._idx))
182 generateE2EEForwardingMaterial: (opts) => getWorker().generateE2EEForwardingMaterial(opts).catch(errorReporter),
183 doesKeySupportE2EEForwarding: async (opts) =>
184 getWorker().doesKeySupportE2EEForwarding(opts).catch(errorReporter),
185 isE2EEForwardingKey: async (opts) => getWorker().isE2EEForwardingKey(opts).catch(errorReporter),
187 replaceUserIDs: async (opts) => {
188 await Promise.all(getAllWorkers().map((worker) => worker.replaceUserIDs(opts)));
190 cloneKeyAndChangeUserIDs: async (opts) => {
191 const [first, ...rest] = getAllWorkers();
192 const keyReference = await first.cloneKeyAndChangeUserIDs(opts).catch(errorReporter);
193 const key = await first.exportPrivateKey({ privateKey: keyReference, passphrase: null, format: 'binary' });
195 rest.map((worker) => worker.importPrivateKey({ binaryKey: key, passphrase: null }, keyReference._idx))
199 exportPublicKey: (opts) => getWorker().exportPublicKey(opts).catch(errorReporter),
200 exportPrivateKey: (opts) => getWorker().exportPrivateKey(opts).catch(errorReporter),
201 clearKeyStore: async () => {
202 await Promise.all(getAllWorkers().map((worker) => worker.clearKeyStore()));
204 clearKey: async (opts) => {
205 await Promise.all(getAllWorkers().map((worker) => worker.clearKey(opts)));
208 isExpiredKey: (opts) => getWorker().isExpiredKey(opts).catch(errorReporter),
209 isRevokedKey: (opts) => getWorker().isRevokedKey(opts).catch(errorReporter),
210 canKeyEncrypt: (opts) => getWorker().canKeyEncrypt(opts).catch(errorReporter),
211 getSHA256Fingerprints: (opts) => getWorker().getSHA256Fingerprints(opts),
212 getMessageInfo: (opts) => getWorker().getMessageInfo(opts).catch(errorReporter),
213 getKeyInfo: (opts) => getWorker().getKeyInfo(opts).catch(errorReporter),
214 getSignatureInfo: (opts) => getWorker().getSignatureInfo(opts).catch(errorReporter),
215 getArmoredKeys: (opts) => getWorker().getArmoredKeys(opts),
216 getArmoredSignature: (opts) => getWorker().getArmoredSignature(opts),
217 getArmoredMessage: (opts) => getWorker().getArmoredMessage(opts),
218 } as WorkerPoolInterface; // casting needed to 'reuse' CryptoApi's parametric types declarations and preserve dynamic inference of
219 // the output types based on the input ones.