2 DEFAULT_KEY_GENERATION_OFFSET,
3 DEFAULT_SIGNATURE_VERIFICATION_OFFSET,
5 } from 'pmcrypto-v6-canary/lib/constants';
7 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
9 import { serverTime } from '../serverTime';
10 import type { ApiInterface } from '../worker/api';
11 import type { WorkerVerifyOptions } from '../worker/api.models';
13 export type CryptoApiInterface = ApiInterface;
15 const assertNotNull = (value: CryptoApiInterface | null): CryptoApiInterface => {
17 throw new Error('CryptoProxy: endpoint not initialized');
22 let endpoint: CryptoApiInterface | null = null;
23 let onEndpointRelease: (endpoint?: any) => Promise<void> = async () => {};
25 interface CryptoProxyInterface extends CryptoApiInterface {
28 * The provided instance must be already initialised and ready to resolve requests.
30 * @param onRelease - callback called after `this.releaseEndpoint()` is invoked and endpoint is released
32 setEndpoint<T extends CryptoApiInterface>(endpoint: T, onRelease?: (endpoint: T) => Promise<void>): void;
35 * Release endpoint. Afterwards, a different one may be set via `setEndpoint()`.
36 * If a `onRelease` callback was passed to `setEndpoint()`, the callback is called before returning.
37 * Note that this function does not have any other side effects, e.g. it does not clear the key store automatically.
38 * Any endpoint-specific clean up logic should be done inside the `onRelease` callback.
40 releaseEndpoint(): Promise<void>;
44 * Prior to OpenPGP.js v5.4.0, trailing spaces were not properly stripped with \r\n line endings (see https://github.com/openpgpjs/openpgpjs/pull/1548).
45 * In order to verify the signatures generated over the incorrectly normalised data, we fallback to not normalising the input.
46 * Currently, this is done inside the CryptoProxy, to transparently track the number of signatures that are affected throughout the apps.
47 * @param options - verification options, with `date` already set to server time
49 async function verifyMessageWithFallback<
50 DataType extends string | Uint8Array,
51 FormatType extends WorkerVerifyOptions<DataType>['format'] = 'utf8',
52 >(options: WorkerVerifyOptions<DataType> & { format?: FormatType }) {
53 const verificationResult = await assertNotNull(endpoint).verifyMessage<DataType, FormatType>(options);
55 const { textData, stripTrailingSpaces } = options;
57 verificationResult.verified === VERIFICATION_STATUS.SIGNED_AND_INVALID &&
58 stripTrailingSpaces &&
60 verificationResult.data !== textData // detect whether some normalisation was applied
62 const fallbackverificationResult = await assertNotNull(endpoint).verifyMessage<string, FormatType>({
64 binaryData: undefined,
65 stripTrailingSpaces: false,
68 if (fallbackverificationResult.verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
69 captureMessage('Fallback verification needed', {
72 return fallbackverificationResult;
75 // detect whether the message has trailing spaces followed by a mix of \r\n and \n line endings
76 const legacyRemoveTrailingSpaces = (text: string) => {
80 let i = line.length - 1;
81 for (; i >= 0 && (line[i] === ' ' || line[i] === '\t'); i--) {}
82 return line.substr(0, i + 1);
87 if (textData !== legacyRemoveTrailingSpaces(textData)) {
88 captureMessage('Fallback verification insufficient', {
94 return verificationResult;
98 * CryptoProxy relays crypto requests to the specified endpoint, which is typically a worker(s), except if
99 * CryptoProxy is already called (also indirectly) from inside a worker.
100 * In such a case, the endpoint can be set to a `new WorkerApi()` instance, or to tbe worker instance itself,
101 * provided it implements/extends WorkerApi.
103 export const CryptoProxy: CryptoProxyInterface = {
104 setEndpoint(endpointInstance, onRelease = onEndpointRelease) {
106 throw new Error('already initialised');
108 endpoint = endpointInstance;
109 onEndpointRelease = onRelease;
113 const tmp = endpoint;
115 return onEndpointRelease(assertNotNull(tmp));
118 encryptMessage: async ({ date = serverTime(), ...opts }) =>
119 assertNotNull(endpoint).encryptMessage({ ...opts, date }),
120 decryptMessage: async ({ date = new Date(+serverTime() + DEFAULT_SIGNATURE_VERIFICATION_OFFSET), ...opts }) =>
121 assertNotNull(endpoint).decryptMessage({ ...opts, date }),
122 signMessage: async ({ date = serverTime(), ...opts }) => assertNotNull(endpoint).signMessage({ ...opts, date }),
123 verifyMessage: async ({ date = new Date(+serverTime() + DEFAULT_SIGNATURE_VERIFICATION_OFFSET), ...opts }) =>
124 verifyMessageWithFallback({ ...opts, date }),
125 verifyCleartextMessage: async ({ date = serverTime(), ...opts }) =>
126 assertNotNull(endpoint).verifyCleartextMessage({ ...opts, date }),
127 processMIME: async ({ date = serverTime(), ...opts }) => assertNotNull(endpoint).processMIME({ ...opts, date }),
129 generateSessionKey: async ({ date = serverTime(), ...opts }) =>
130 assertNotNull(endpoint).generateSessionKey({ ...opts, date }),
131 generateSessionKeyForAlgorithm: async (opts) => assertNotNull(endpoint).generateSessionKeyForAlgorithm(opts),
132 encryptSessionKey: async ({ date = serverTime(), ...opts }) =>
133 assertNotNull(endpoint).encryptSessionKey({ ...opts, date }),
134 decryptSessionKey: async ({ date = serverTime(), ...opts }) =>
135 assertNotNull(endpoint).decryptSessionKey({ ...opts, date }),
137 importPrivateKey: async (opts) => assertNotNull(endpoint).importPrivateKey(opts),
138 importPublicKey: async (opts) => assertNotNull(endpoint).importPublicKey(opts),
139 generateKey: async ({ date = new Date(+serverTime() + DEFAULT_KEY_GENERATION_OFFSET), ...opts }) =>
140 assertNotNull(endpoint).generateKey({ ...opts, date }),
141 reformatKey: async ({ privateKey, date = privateKey.getCreationTime(), ...opts }) =>
142 assertNotNull(endpoint).reformatKey({ ...opts, privateKey, date }),
143 exportPublicKey: async (opts) => assertNotNull(endpoint).exportPublicKey(opts),
144 exportPrivateKey: async (opts) => assertNotNull(endpoint).exportPrivateKey(opts),
145 clearKeyStore: () => assertNotNull(endpoint).clearKeyStore(),
146 clearKey: async (opts) => assertNotNull(endpoint).clearKey(opts),
147 replaceUserIDs: async (opts) => assertNotNull(endpoint).replaceUserIDs(opts),
148 cloneKeyAndChangeUserIDs: async (opts) => assertNotNull(endpoint).cloneKeyAndChangeUserIDs(opts),
149 generateE2EEForwardingMaterial: async ({ date = serverTime(), ...opts }) =>
150 assertNotNull(endpoint).generateE2EEForwardingMaterial({ ...opts, date }),
151 doesKeySupportE2EEForwarding: async ({ date = serverTime(), ...opts }) =>
152 assertNotNull(endpoint).doesKeySupportE2EEForwarding({ ...opts, date }),
153 isE2EEForwardingKey: async ({ date = serverTime(), ...opts }) =>
154 assertNotNull(endpoint).isE2EEForwardingKey({ ...opts, date }),
156 isRevokedKey: async ({ date = serverTime(), ...opts }) => assertNotNull(endpoint).isRevokedKey({ ...opts, date }),
157 isExpiredKey: async ({ date = serverTime(), ...opts }) => assertNotNull(endpoint).isExpiredKey({ ...opts, date }),
158 canKeyEncrypt: async ({ date = serverTime(), ...opts }) => assertNotNull(endpoint).canKeyEncrypt({ ...opts, date }),
159 getSHA256Fingerprints: async (opts) => assertNotNull(endpoint).getSHA256Fingerprints(opts),
160 computeHash: async (opts) => assertNotNull(endpoint).computeHash(opts),
161 computeHashStream: async (opts) => assertNotNull(endpoint).computeHashStream(opts),
162 computeArgon2: (opts) => assertNotNull(endpoint).computeArgon2(opts),
164 getArmoredMessage: async (opts) => assertNotNull(endpoint).getArmoredMessage(opts),
165 getArmoredKeys: async (opts) => assertNotNull(endpoint).getArmoredKeys(opts),
166 getArmoredSignature: async (opts) => assertNotNull(endpoint).getArmoredSignature(opts),
167 getSignatureInfo: async (opts) => assertNotNull(endpoint).getSignatureInfo(opts),
168 getMessageInfo: async (opts) => assertNotNull(endpoint).getMessageInfo(opts),
169 getKeyInfo: async (opts) => assertNotNull(endpoint).getKeyInfo(opts),