Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / crypto / lib / worker / api.ts
blob4cfb1a917389c63ddf4e81513f80d957efe84676
1 /* eslint-disable class-methods-use-this */
3 /* eslint-disable max-classes-per-file */
5 /* eslint-disable no-underscore-dangle */
6 import type { AlgorithmInfo as AlgorithmInfoV5, Argon2Options, Data, Key, PrivateKey, PublicKey } from 'pmcrypto';
7 import {
8     SHA256,
9     SHA512,
10     argon2,
11     armorBytes,
12     canKeyEncrypt,
13     checkKeyCompatibility,
14     checkKeyStrength,
15     decryptKey,
16     decryptMessage,
17     decryptSessionKey,
18     doesKeySupportForwarding,
19     encryptKey,
20     encryptMessage,
21     encryptSessionKey,
22     generateForwardingMaterial,
23     generateKey,
24     generateSessionKey,
25     generateSessionKeyForAlgorithm,
26     getSHA256Fingerprints,
27     init as initPmcrypto,
28     isExpiredKey,
29     isForwardingKey,
30     isRevokedKey,
31     processMIME,
32     readCleartextMessage,
33     readKey,
34     readKeys,
35     readMessage,
36     readPrivateKey,
37     readSignature,
38     reformatKey,
39     signMessage,
40     unsafeMD5,
41     unsafeSHA1,
42     verifyCleartextMessage,
43     verifyMessage,
44 } from 'pmcrypto';
45 import type { SubkeyOptions, UserID } from 'pmcrypto/lib/openpgp';
46 import { enums } from 'pmcrypto/lib/openpgp';
48 import { ARGON2_PARAMS } from '../constants';
49 import { arrayToHexString } from '../utils';
50 import type {
51     AlgorithmInfo,
52     ComputeHashStreamOptions,
53     KeyInfo,
54     KeyReference,
55     MaybeArray,
56     MessageInfo,
57     PrivateKeyReference,
58     PublicKeyReference,
59     SessionKey as SessionKeyWithoutPlaintextAlgo, // OpenPGP.js v5 has a 'plaintext' algo value for historical reasons.
60     SignatureInfo,
61     WorkerDecryptionOptions,
62     WorkerEncryptOptions,
63     WorkerEncryptSessionKeyOptions,
64     WorkerGenerateKeyOptions,
65     WorkerGenerateSessionKeyOptions,
66     WorkerGetKeyInfoOptions,
67     WorkerGetMessageInfoOptions,
68     WorkerGetSignatureInfoOptions,
69     WorkerImportPrivateKeyOptions,
70     WorkerImportPublicKeyOptions,
71     WorkerProcessMIMEOptions,
72     WorkerReformatKeyOptions,
73     WorkerSignOptions,
74     WorkerVerifyCleartextOptions,
75     WorkerVerifyOptions,
76 } from './api.models';
78 // Note:
79 // - streams are currently not supported since they are not Transferable (not in all browsers).
80 // - when returning binary data, the values are always transferred.
82 type SerializedSignatureOptions = { armoredSignature?: string; binarySignature?: Uint8Array };
83 const getSignature = async ({ armoredSignature, binarySignature }: SerializedSignatureOptions) => {
84     if (armoredSignature) {
85         return readSignature({ armoredSignature });
86     } else if (binarySignature) {
87         return readSignature({ binarySignature });
88     }
89     throw new Error('Must provide `armoredSignature` or `binarySignature`');
92 type SerializedMessageOptions = { armoredMessage?: string; binaryMessage?: Uint8Array };
93 const getMessage = async ({ armoredMessage, binaryMessage }: SerializedMessageOptions) => {
94     if (armoredMessage) {
95         return readMessage({ armoredMessage });
96     } else if (binaryMessage) {
97         return readMessage({ binaryMessage });
98     }
99     throw new Error('Must provide `armoredMessage` or `binaryMessage`');
102 type SerializedKeyOptions = { armoredKey?: string; binaryKey?: Uint8Array };
103 const getKey = async ({ armoredKey, binaryKey }: SerializedKeyOptions) => {
104     if (armoredKey) {
105         return readKey({ armoredKey });
106     } else if (binaryKey) {
107         return readKey({ binaryKey });
108     }
109     throw new Error('Must provide `armoredKey` or `binaryKey`');
112 const toArray = <T>(maybeArray: MaybeArray<T>) => (Array.isArray(maybeArray) ? maybeArray : [maybeArray]);
114 const getPublicKeyReference = async (key: PublicKey, keyStoreID: number): Promise<PublicKeyReference> => {
115     const publicKey = key.isPrivate() ? key.toPublic() : key; // We don't throw on private key since we allow importing an (encrypted) private key using 'importPublicKey'
116     const v5Tov6AlgorithmInfo = (algorithmInfo: AlgorithmInfoV5): AlgorithmInfo => {
117         const v5ToV6Curve = (curveName: AlgorithmInfoV5['curve']): AlgorithmInfo['curve'] => {
118             switch (curveName) {
119                 case 'curve25519':
120                     return 'curve25519Legacy';
121                 case 'ed25519':
122                     return 'ed25519Legacy';
123                 case 'p256':
124                     return 'nistP256';
125                 case 'p384':
126                     return 'nistP384';
127                 case 'p521':
128                     return 'nistP521';
129                 default:
130                     return curveName;
131             }
132         };
133         switch (algorithmInfo.algorithm) {
134             case 'eddsa':
135                 return {
136                     algorithm: 'eddsaLegacy',
137                     curve: 'ed25519Legacy',
138                 };
139             case 'ecdh':
140                 return {
141                     algorithm: 'ecdh',
142                     curve: v5ToV6Curve(algorithmInfo.curve),
143                 };
144             default:
145                 const result: AlgorithmInfo = { algorithm: algorithmInfo.algorithm };
146                 if (algorithmInfo.curve !== undefined) {
147                     result.curve = v5ToV6Curve(algorithmInfo.curve);
148                 }
149                 if (algorithmInfo.bits !== undefined) {
150                     result.bits = algorithmInfo.bits;
151                 }
152                 return result;
153         }
154     };
156     const fingerprint = publicKey.getFingerprint();
157     const hexKeyID = publicKey.getKeyID().toHex();
158     const hexKeyIDs = publicKey.getKeyIDs().map((id) => id.toHex());
159     const algorithmInfo = publicKey.getAlgorithmInfo();
160     const creationTime = publicKey.getCreationTime();
161     const expirationTime = await publicKey.getExpirationTime();
162     const userIDs = publicKey.getUserIDs();
163     const keyContentHash = await SHA256(publicKey.write()).then(arrayToHexString);
164     // Allow comparing keys without third-party certification
165     let keyContentHashNoCerts: string;
166     // Check if third-party certs are present
167     if (publicKey.users.some((user) => user.otherCertifications.length > 0)) {
168         // @ts-ignore missing `clone()` definition
169         const publicKeyClone: PublicKey = publicKey.clone();
170         publicKeyClone.users.forEach((user) => {
171             user.otherCertifications = [];
172         });
173         keyContentHashNoCerts = await SHA256(publicKeyClone.write()).then(arrayToHexString);
174     } else {
175         keyContentHashNoCerts = keyContentHash;
176     }
178     let isWeak: boolean;
179     try {
180         checkKeyStrength(publicKey);
181         isWeak = false;
182     } catch {
183         isWeak = true;
184     }
185     let compatibilityError: Error;
186     try {
187         checkKeyCompatibility(publicKey);
188     } catch (err: any) {
189         compatibilityError = err;
190     }
191     return {
192         _idx: keyStoreID,
193         _keyContentHash: [keyContentHash, keyContentHashNoCerts],
194         _getCompatibilityError: () => compatibilityError,
195         isPrivate: () => false,
196         getFingerprint: () => fingerprint,
197         getKeyID: () => hexKeyID,
198         getKeyIDs: () => hexKeyIDs,
199         getAlgorithmInfo: () => v5Tov6AlgorithmInfo(algorithmInfo),
200         getCreationTime: () => creationTime,
201         getExpirationTime: () => expirationTime,
202         getUserIDs: () => userIDs,
203         isWeak: () => isWeak,
204         equals: (otherKey: KeyReference, ignoreOtherCerts = false) =>
205             ignoreOtherCerts
206                 ? otherKey._keyContentHash[1] === keyContentHashNoCerts
207                 : otherKey._keyContentHash[0] === keyContentHash,
208         subkeys: publicKey.getSubkeys().map((subkey) => {
209             const subkeyAlgoInfo = v5Tov6AlgorithmInfo(subkey.getAlgorithmInfo());
210             const subkeyKeyID = subkey.getKeyID().toHex();
211             return {
212                 getAlgorithmInfo: () => subkeyAlgoInfo,
213                 getKeyID: () => subkeyKeyID,
214             };
215         }),
216     } as PublicKeyReference;
219 const getPrivateKeyReference = async (privateKey: PrivateKey, keyStoreID: number): Promise<PrivateKeyReference> => {
220     const publicKeyReference = await getPublicKeyReference(privateKey.toPublic(), keyStoreID);
221     return {
222         ...publicKeyReference,
223         isPrivate: () => true,
224         _dummyType: 'private',
225     } as PrivateKeyReference;
228 class KeyStore {
229     private store = new Map<number, Key>();
231     /**
232      * Monotonic counter keeping track of the next unique identifier to index a newly added key.
233      * The starting counter value is picked at random to minimize the changes of collisions between keys during different user sessions.
234      * NB: key references may be stored by webapps even after the worker has been destroyed (e.g. after closing the browser window),
235      * hence we want to keep using different identifiers even after restarting the worker, to also invalidate those stale key references.
236      */
237     private nextIdx = crypto.getRandomValues(new Uint32Array(1))[0];
239     /**
240      * Add a key to the key store.
241      * @param key - key to add
242      * @param customIdx - custom identifier to use to store the key, instead of the internally generated one.
243      *                    This argument is primarily intended for when key store identifiers need to be synchronised across different workers.
244      *                    This value must be unique for each key, even across different sessions.
245      * @returns key identifier to retrieve the key from the store
246      */
247     add(key: Key, customIdx?: number) {
248         const idx = customIdx !== undefined ? customIdx : this.nextIdx;
249         if (this.store.has(idx)) {
250             throw new Error(`Idx ${idx} already in use`);
251         }
252         this.store.set(idx, key);
253         this.nextIdx++; // increment regardless of customIdx, for code simplicity
254         return idx;
255     }
257     get(idx: number) {
258         const key = this.store.get(idx);
259         if (!key) {
260             throw new Error('Key not found');
261         }
262         return key;
263     }
265     clearAll() {
266         this.store.forEach((key) => {
267             if (key.isPrivate()) {
268                 // @ts-ignore missing definition for clearPrivateParams()
269                 key.clearPrivateParams();
270             }
271         });
272         this.store.clear();
273         // no need to reset index
274     }
276     clear(idx: number) {
277         const keyToClear = this.get(idx);
278         if (keyToClear.isPrivate()) {
279             // @ts-ignore missing definition for clearPrivateParams()
280             keyToClear.clearPrivateParams();
281         }
282         this.store.delete(idx);
283     }
286 type SerialisedOutputFormat = 'armored' | 'binary' | undefined;
287 type SerialisedOutputTypeFromFormat<F extends SerialisedOutputFormat> = F extends 'armored'
288     ? string
289     : F extends 'binary'
290       ? Uint8Array
291       : never;
293 class KeyManagementApi {
294     protected keyStore = new KeyStore();
296     /**
297      * Invalidate all key references by removing all keys from the internal key store.
298      * The private key material corresponding to any PrivateKeyReference is erased from memory.
299      */
300     async clearKeyStore() {
301         this.keyStore.clearAll();
302     }
304     /**
305      * Invalidate the key reference by removing the key from the internal key store.
306      * If a PrivateKeyReference is given, the private key material is erased from memory.
307      */
308     async clearKey({ key: keyReference }: { key: KeyReference }) {
309         this.keyStore.clear(keyReference._idx);
310     }
312     /**
313      * Generate a key for the given UserID.
314      * The key is stored in the key store, and can be exported using `exportPrivateKey` or `exportPublicKey`.
315      * @param options.userIDs - user IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }`
316      * @param options.type - key algorithm type: ECC (default) or RSA
317      * @param options.rsaBits - number of bits for RSA keys
318      * @param options.curve - elliptic curve for ECC keys
319      * @param options.keyExpirationTime- number of seconds from the key creation time after which the key expires
320      * @param options.subkeys - options for each subkey e.g. `[{ sign: true, passphrase: '123'}]`
321      * @param options.date - use the given date as creation date of the key and the key signatures, instead of the server time
322      * @returns reference to the generated private key
323      */
324     async generateKey(options: WorkerGenerateKeyOptions) {
325         const v6Tov5CurveOption = (curve: WorkerGenerateKeyOptions['curve']) => {
326             switch (curve) {
327                 case 'ed25519Legacy':
328                 case 'curve25519Legacy':
329                     return 'ed25519';
330                 case 'nistP256':
331                     return 'p256';
332                 case 'nistP384':
333                     return 'p384';
334                 case 'nistP521':
335                     return 'p521';
336                 default:
337                     return curve;
338             }
339         };
341         const { privateKey } = await generateKey({
342             ...options,
343             curve: v6Tov5CurveOption(options.curve),
344             subkeys: options.subkeys?.map<SubkeyOptions>((subkeyOptions) => ({
345                 ...subkeyOptions,
346                 curve: v6Tov5CurveOption(subkeyOptions.curve),
347             })),
348             format: 'object',
349         });
350         // Typescript guards against a passphrase input, but it's best to ensure the option wasn't given since for API simplicity we assume any PrivateKeyReference points to a decrypted key.
351         if (!privateKey.isDecrypted()) {
352             throw new Error(
353                 'Unexpected "passphrase" option on key generation. Use "exportPrivateKey" after key generation to obtain a transferable encrypted key.'
354             );
355         }
356         const keyStoreID = this.keyStore.add(privateKey);
358         return getPrivateKeyReference(privateKey, keyStoreID);
359     }
361     async reformatKey({ privateKey: keyReference, ...options }: WorkerReformatKeyOptions) {
362         const originalKey = this.keyStore.get(keyReference._idx) as PrivateKey;
363         // we have to deep clone before reformatting, since privateParams of reformatted key point to the ones of the given privateKey, and
364         // we do not want reformatted key to be affected if the original key reference is cleared/deleted.
365         // @ts-ignore - missing .clone() definition
366         const keyToReformat = originalKey.clone(true);
367         const { privateKey } = await reformatKey({ ...options, privateKey: keyToReformat, format: 'object' });
368         // Typescript guards against a passphrase input, but it's best to ensure the option wasn't given since for API simplicity we assume any PrivateKeyReference points to a decrypted key.
369         if (!privateKey.isDecrypted()) {
370             throw new Error(
371                 'Unexpected "passphrase" option on key reformat. Use "exportPrivateKey" after key reformatting to obtain a transferable encrypted key.'
372             );
373         }
374         const keyStoreID = this.keyStore.add(privateKey);
376         return getPrivateKeyReference(privateKey, keyStoreID);
377     }
379     /**
380      * Import a private key, which is either already decrypted, or that can be decrypted with the given passphrase.
381      * If a passphrase is given, but the key is already decrypted, importing fails.
382      * Either `armoredKey` or `binaryKey` must be provided.
383      * Note: if the passphrase to decrypt the key is unknown, the key shuld be imported using `importPublicKey` instead.
384      * @param options.passphrase - key passphrase if the input key is encrypted, or `null` if the input key is expected to be already decrypted
385      * @returns reference to imported private key
386      * @throws {Error} if the key cannot be decrypted or importing fails
387      */
388     async importPrivateKey<T extends Data>(
389         { armoredKey, binaryKey, passphrase, checkCompatibility }: WorkerImportPrivateKeyOptions<T>,
390         _customIdx?: number
391     ) {
392         if (!armoredKey && !binaryKey) {
393             throw new Error('Must provide `armoredKey` or `binaryKey`');
394         }
395         const expectDecrypted = passphrase === null;
396         const maybeEncryptedKey = binaryKey
397             ? await readPrivateKey({ binaryKey })
398             : await readPrivateKey({ armoredKey: armoredKey! });
399         if (checkCompatibility) {
400             checkKeyCompatibility(maybeEncryptedKey);
401         }
402         let decryptedKey;
403         if (expectDecrypted) {
404             if (!maybeEncryptedKey.isDecrypted()) {
405                 throw new Error('Provide passphrase to import an encrypted private key');
406             }
407             decryptedKey = maybeEncryptedKey;
408             // @ts-ignore missing .validate() types
409             await decryptedKey.validate();
410         } else {
411             const usesArgon2 = maybeEncryptedKey.getKeys().some(
412                 // @ts-ignore s2k field not declared
413                 (keyOrSubkey) => keyOrSubkey.keyPacket.s2k && keyOrSubkey.keyPacket.s2k.type === 'argon2'
414             );
415             if (usesArgon2) {
416                 // TODO: Argon2 uses Wasm which requires special bundling
417                 throw new Error('Keys encrypted using Argon2 are not supported yet');
418             }
419             decryptedKey = await decryptKey({ privateKey: maybeEncryptedKey, passphrase });
420         }
422         const keyStoreID = this.keyStore.add(decryptedKey, _customIdx);
424         return getPrivateKeyReference(decryptedKey, keyStoreID);
425     }
427     /**
428      * Import a public key.
429      * Either `armoredKey` or `binaryKey` must be provided.
430      * Note: if a private key is given, it will be converted to a public key before import.
431      * @returns reference to imported public key
432      */
433     async importPublicKey<T extends Data>(
434         { armoredKey, binaryKey, checkCompatibility }: WorkerImportPublicKeyOptions<T>,
435         _customIdx?: number
436     ) {
437         const publicKey = await getKey({ binaryKey, armoredKey });
438         if (checkCompatibility) {
439             checkKeyCompatibility(publicKey);
440         }
441         const keyStoreID = this.keyStore.add(publicKey, _customIdx);
442         return getPublicKeyReference(publicKey, keyStoreID);
443     }
445     /**
446      * Get the serialized public key.
447      * Exporting a key does not invalidate the corresponding `KeyReference`, nor does it remove the key from internal storage (use `clearKey()` for that).
448      * @param options.format - `'binary'` or `'armored'` format of serialized key
449      * @returns serialized public key
450      */
451     async exportPublicKey<F extends SerialisedOutputFormat = 'armored'>({
452         format = 'armored',
453         key: keyReference,
454     }: {
455         key: KeyReference;
456         format?: F;
457     }): Promise<SerialisedOutputTypeFromFormat<F>> {
458         const maybePrivateKey = this.keyStore.get(keyReference._idx);
459         const publicKey = maybePrivateKey.isPrivate() ? maybePrivateKey.toPublic() : maybePrivateKey;
460         const serializedKey = format === 'binary' ? publicKey.write() : publicKey.armor();
461         return serializedKey as SerialisedOutputTypeFromFormat<F>;
462     }
464     /**
465      * Get the serialized private key, encrypted with the given `passphrase`.
466      * Exporting a key does not invalidate the corresponding `keyReference`, nor does it remove the key from internal storage (use `clearKey()` for that).
467      * @param options.passphrase - passphrase to encrypt the key with (non-empty string), or `null` to export an unencrypted key (not recommended).
468      * @param options.format - `'binary'` or `'armored'` format of serialized key
469      * @returns serialized encrypted key
470      */
471     async exportPrivateKey<F extends SerialisedOutputFormat = 'armored'>({
472         format = 'armored',
473         ...options
474     }: {
475         privateKey: PrivateKeyReference;
476         passphrase: string | null;
477         format?: F;
478     }): Promise<SerialisedOutputTypeFromFormat<F>> {
479         const { privateKey: keyReference, passphrase } = options;
480         if (!keyReference.isPrivate()) {
481             throw new Error('Private key expected');
482         }
483         const privateKey = this.keyStore.get(keyReference._idx) as PrivateKey;
484         const doNotEncrypt = passphrase === null;
485         const maybeEncryptedKey = doNotEncrypt ? privateKey : await encryptKey({ privateKey, passphrase });
487         const serializedKey = format === 'binary' ? maybeEncryptedKey.write() : maybeEncryptedKey.armor();
488         return serializedKey as SerialisedOutputTypeFromFormat<F>;
489     }
493  * Each instance keeps a dedicated key storage.
494  */
495 export class Api extends KeyManagementApi {
496     /**
497      * Init pmcrypto and set the underlying global OpenPGP config.
498      */
499     static init() {
500         initPmcrypto();
501     }
503     /**
504      * Encrypt the given data using `encryptionKeys`, `sessionKeys` and `passwords`, after optionally
505      * signing it with `signingKeys`.
506      * Either `textData` or `binaryData` must be given.
507      * A detached signature over the data may be provided by passing either `armoredSignature` or `binarySignature`.
508      * @param options.textData - text data to encrypt
509      * @param options.binaryData - binary data to encrypt
510      * @param options.stripTrailingSpaces - whether trailing spaces should be removed from each line of `textData`
511      * @param options.context - (signed data only) settings to prevent verifying the signature in a different context (signature domain separation)
512      * @param options.format - `'binary` or `'armored'` format of serialized signed message
513      * @param options.date - use the given date for the message signature, instead of the server time
514      */
515     async encryptMessage<
516         DataType extends Data,
517         FormatType extends WorkerEncryptOptions<DataType>['format'] = 'armored',
518         DetachedType extends boolean = false,
519     >({
520         encryptionKeys: encryptionKeyRefs = [],
521         signingKeys: signingKeyRefs = [],
522         armoredSignature,
523         binarySignature,
524         compress = false,
525         config = {},
526         ...options
527     }: WorkerEncryptOptions<DataType> & { format?: FormatType; detached?: DetachedType }) {
528         const signingKeys = toArray(signingKeyRefs).map(
529             (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
530         );
531         const encryptionKeys = toArray(encryptionKeyRefs).map(
532             (keyReference) => this.keyStore.get(keyReference._idx) as PublicKey
533         );
534         const inputSignature =
535             binarySignature || armoredSignature ? await getSignature({ armoredSignature, binarySignature }) : undefined;
537         if (config.preferredCompressionAlgorithm) {
538             throw new Error(
539                 'Passing `config.preferredCompressionAlgorithm` is not supported. Use `compress` option instead.'
540             );
541         }
543         const encryptionResult = await encryptMessage<DataType, FormatType, DetachedType>({
544             ...options,
545             // @ts-ignore probably issue with mismatching underlying stream definitions
546             textData: options.textData,
547             encryptionKeys,
548             signingKeys,
549             signature: inputSignature,
550             config: {
551                 ...config,
552                 preferredCompressionAlgorithm: compress ? enums.compression.zlib : enums.compression.uncompressed,
553             },
554         });
556         return encryptionResult;
557     }
559     /**
560      * Create a signature over the given data using `signingKeys`.
561      * Either `textData` or `binaryData` must be given.
562      * @param options.textData - text data to sign
563      * @param options.binaryData - binary data to sign
564      * @param options.stripTrailingSpaces - whether trailing spaces should be removed from each line of `textData`
565      * @param options.context - settings to prevent verifying the signature in a different context (signature domain separation)
566      * @param options.detached - whether to return a detached signature, without the signed data
567      * @param options.format - `'binary` or `'armored'` format of serialized signed message
568      * @param options.date - use the given date for signing, instead of the server time
569      * @returns serialized signed message or signature
570      */
571     async signMessage<
572         DataType extends Data,
573         FormatType extends WorkerSignOptions<DataType>['format'] = 'armored',
574         // inferring D (detached signature type) is unnecessary since the result type does not depend on it for format !== 'object'
575     >({ signingKeys: signingKeyRefs = [], ...options }: WorkerSignOptions<DataType> & { format?: FormatType }) {
576         const signingKeys = toArray(signingKeyRefs).map(
577             (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
578         );
579         const signResult = await signMessage<DataType, FormatType, boolean>({
580             ...options,
581             // @ts-ignore probably issue with mismatching underlying stream definitions
582             textData: options.textData,
583             signingKeys,
584         });
586         return signResult;
587     }
589     /**
590      * Verify a signature over the given data.
591      * Either `armoredSignature` or `binarySignature` must be given for the signature, and either `textData` or `binaryData` must be given as data to be verified.
592      * To verify a Cleartext message, which includes both the signed data and the corresponding signature, see `verifyCleartextMessage`.
593      * @param options.textData - expected signed text data
594      * @param options.binaryData - expected signed binary data
595      * @param options.armoredSignature - armored signature to verify
596      * @param options.binarySignature - binary signature to verify
597      * @param options.stripTrailingSpaces - whether trailing spaces should be removed from each line of `textData`.
598      *                                      This option must match the one used when signing.
599      * @param options.context - settings to prevent verifying a signature from a different context (signature domain separation).
600      *                          This option should match the one used when signing.
601      * @returns signature verification result over the given data
602      */
603     async verifyMessage<DataType extends Data, FormatType extends WorkerVerifyOptions<DataType>['format'] = 'utf8'>({
604         armoredSignature,
605         binarySignature,
606         verificationKeys: verificationKeyRefs = [],
607         ...options
608     }: WorkerVerifyOptions<DataType> & { format?: FormatType }) {
609         const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
610             this.keyStore.get(keyReference._idx)
611         );
612         const signature = await getSignature({ armoredSignature, binarySignature });
613         const {
614             signatures: signatureObjects, // extracting this is needed for proper type inference of `serialisedResult.signatures`
615             ...verificationResultWithoutSignatures
616         } = await verifyMessage<DataType, FormatType>({ signature, verificationKeys, ...options });
618         const serialisedResult = {
619             ...verificationResultWithoutSignatures,
620             signatures: signatureObjects.map((sig) => sig.write() as Uint8Array), // no support for streamed input for now
621         };
623         return serialisedResult;
624     }
626     /**
627      * Verify a Cleartext message, which includes the signed data and the corresponding signature.
628      * A cleartext message is always in armored form.
629      * To verify a detached signature over some data, see `verifyMessage` instead.
630      * @params options.armoredCleartextSignature - armored cleartext message to verify
631      */
632     async verifyCleartextMessage({
633         armoredCleartextMessage,
634         verificationKeys: verificationKeyRefs = [],
635         ...options
636     }: WorkerVerifyCleartextOptions) {
637         const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
638             this.keyStore.get(keyReference._idx)
639         );
640         const cleartextMessage = await readCleartextMessage({ cleartextMessage: armoredCleartextMessage });
641         const {
642             signatures: signatureObjects, // extracting this is needed for proper type inference of `serialisedResult.signatures`
643             ...verificationResultWithoutSignatures
644         } = await verifyCleartextMessage({ cleartextMessage, verificationKeys, ...options });
646         const serialisedResult = {
647             ...verificationResultWithoutSignatures,
648             signatures: signatureObjects.map((sig) => sig.write() as Uint8Array), // no support for streamed input for now
649         };
651         return serialisedResult;
652     }
654     /**
655      * Decrypt a message using `decryptionKeys`, `sessionKey`, or `passwords`, and optionally verify the content using `verificationKeys`.
656      * Eiher `armoredMessage` or `binaryMessage` must be given.
657      * For detached signature verification over the decrypted data, one of `armoredSignature`,
658      * `binarySignature`, `armoredEncryptedSignature` and `binaryEncryptedSignature` may be given.
659      * @param options.armoredMessage - armored data to decrypt
660      * @param options.binaryMessage - binary data to decrypt
661      * @param options.expectSigned - if true, data decryption fails if the message is not signed with the provided `verificationKeys`
662      * @param options.context - (signed data only) settings to prevent verifying a signature from a different context (signature domain separation).
663      *                          This option should match the one used when encrypting.
664      * @param options.format - whether to return data as a string or Uint8Array. If 'utf8' (the default), also normalize newlines.
665      * @param options.date - use the given date for verification instead of the server time
666      */
667     async decryptMessage<FormatType extends WorkerDecryptionOptions['format'] = 'utf8'>({
668         decryptionKeys: decryptionKeyRefs = [],
669         verificationKeys: verificationKeyRefs = [],
670         armoredMessage,
671         binaryMessage,
672         armoredSignature,
673         binarySignature,
674         armoredEncryptedSignature: armoredEncSignature,
675         binaryEncryptedSignature: binaryEncSingature,
676         ...options
677     }: WorkerDecryptionOptions & { format?: FormatType }) {
678         const decryptionKeys = toArray(decryptionKeyRefs).map(
679             (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
680         );
681         const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
682             this.keyStore.get(keyReference._idx)
683         );
685         const message = await getMessage({ binaryMessage, armoredMessage });
686         const signature =
687             binarySignature || armoredSignature ? await getSignature({ binarySignature, armoredSignature }) : undefined;
688         const encryptedSignature =
689             binaryEncSingature || armoredEncSignature
690                 ? await getMessage({ binaryMessage: binaryEncSingature, armoredMessage: armoredEncSignature })
691                 : undefined;
693         const { signatures: signatureObjects, ...decryptionResultWithoutSignatures } = await decryptMessage<
694             Data,
695             FormatType
696         >({
697             ...options,
698             message,
699             signature,
700             encryptedSignature,
701             decryptionKeys,
702             verificationKeys,
703         });
705         const serialisedResult = {
706             ...decryptionResultWithoutSignatures,
707             signatures: signatureObjects.map((sig) => sig.write() as Uint8Array), // no support for streamed input for now
708         };
710         return serialisedResult;
712         // TODO: once we have support for the intendedRecipient verification, we should add the
713         // a `verify(publicKeys)` function to the decryption result, that allows verifying
714         // the decrypted signatures after decryption.
715         // Note: asking the apps to call `verifyMessage` separately is not an option, since
716         // the verification result is to be considered invalid outside of the encryption context if the intended recipient is present, see: https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#section-5.2.3.32
717     }
719     /**
720      * Generate forwardee key and proxy parameter needed to setup end-to-end encrypted forwarding for the given
721      * privateKey.
722      * @param options.forwarderPrivateKey - private key of original recipient, initiating the forwarding
723      * @param options.userIDsForForwardeeKey - userIDs to attach to forwardee key
724      * @param options.passphrase - passphrase to encrypt the generated forwardee key with
725      * @param options.date - date to use as key creation time, instead of server time
726      */
727     async generateE2EEForwardingMaterial({
728         forwarderKey,
729         userIDsForForwardeeKey,
730         passphrase,
731         date,
732     }: {
733         forwarderKey: PrivateKeyReference;
734         userIDsForForwardeeKey: MaybeArray<UserID>;
735         passphrase: string | null;
736         date?: Date;
737     }) {
738         const originalKey = this.keyStore.get(forwarderKey._idx) as PrivateKey;
740         const { proxyInstances, forwardeeKey } = await generateForwardingMaterial(
741             originalKey,
742             userIDsForForwardeeKey,
743             date
744         );
746         const maybeEncryptedKey = passphrase
747             ? await encryptKey({ privateKey: forwardeeKey, passphrase })
748             : forwardeeKey;
750         return {
751             forwardeeKey: maybeEncryptedKey.armor(),
752             proxyInstances,
753         };
754     }
756     /**
757      * Check whether a key can be used as input to `generateE2EEForwardingMaterial` to setup E2EE forwarding.
758      */
759     async doesKeySupportE2EEForwarding({
760         forwarderKey: keyReference,
761         date,
762     }: {
763         forwarderKey: PrivateKeyReference;
764         date?: Date;
765     }) {
766         const key = this.keyStore.get(keyReference._idx);
767         if (!key.isPrivate()) {
768             return false;
769         }
770         const supportsForwarding = await doesKeySupportForwarding(key, date);
771         return supportsForwarding;
772     }
774     /**
775      * Whether a key is a E2EE forwarding recipient key, where all its encryption-capable (sub)keys are setup
776      * for forwarding.
777      * NB: this function also accepts `PublicKeyReference`s in order to determine the status of inactive (undecryptable)
778      * private keys. Such keys can only be imported using `importPublicKey`, but it's important that the encrypted
779      * private key is imported (not the corresponding public key).
780      * @throws if a PublicKeyReference containing a public key is given
781      */
782     async isE2EEForwardingKey({ key: keyReference, date }: { key: KeyReference; date?: Date }) {
783         // We support PublicKeyReference to determine the status of inactive/undecryptable address keys.
784         // A PublicKeyReference can contain an encrypted private key.
785         const key = this.keyStore.get(keyReference._idx);
786         if (!key.isPrivate()) {
787             throw new Error('Unexpected public key');
788         }
789         const forForwarding = await isForwardingKey(key, date);
790         return forForwarding;
791     }
793     /**
794      * Generating a session key for the specified symmetric algorithm.
795      * To generate a session key based on some recipient's public key preferences,
796      * use `generateSessionKey()` instead.
797      */
798     async generateSessionKeyForAlgorithm(algoName: Parameters<typeof generateSessionKeyForAlgorithm>[0]) {
799         const sessionKeyBytes = await generateSessionKeyForAlgorithm(algoName);
800         return sessionKeyBytes;
801     }
803     /**
804      * Generate a session key compatible with the given recipient keys.
805      * To get a session key for a specific symmetric algorithm, use `generateSessionKeyForAlgorithm` instead.
806      */
807     async generateSessionKey({ recipientKeys: recipientKeyRefs = [], ...options }: WorkerGenerateSessionKeyOptions) {
808         const recipientKeys = toArray(recipientKeyRefs).map((keyReference) => this.keyStore.get(keyReference._idx));
809         const sessionKey = await generateSessionKey({ recipientKeys, ...options });
810         return sessionKey as SessionKeyWithoutPlaintextAlgo;
811     }
813     /**
814      * Encrypt a session key with `encryptionKeys`, `passwords`, or both at once.
815      * At least one of `encryptionKeys` or `passwords` must be specified.
816      * @param options.data - the session key to be encrypted e.g. 16 random bytes (for aes128)
817      * @param options.algorithm - algorithm of the session key
818      * @param options.aeadAlgorithm - AEAD algorithm of the session key
819      * @param options.format - `'armored'` or `'binary'` format of the returned encrypted message
820      * @param options.wildcard - use a key ID of 0 instead of the encryption key IDs
821      * @param options.date - use the given date for key validity checks, instead of the server time
822      */
823     async encryptSessionKey<FormatType extends WorkerEncryptSessionKeyOptions['format'] = 'armored'>({
824         encryptionKeys: encryptionKeyRefs = [],
825         ...options
826     }: WorkerEncryptSessionKeyOptions & { format?: FormatType }): Promise<SerialisedOutputTypeFromFormat<FormatType>> {
827         const encryptionKeys = toArray(encryptionKeyRefs).map(
828             (keyReference) => this.keyStore.get(keyReference._idx) as PublicKey
829         );
830         const encryptedData = await encryptSessionKey<FormatType>({
831             ...options,
832             encryptionKeys,
833         });
835         return encryptedData as SerialisedOutputTypeFromFormat<FormatType>;
836     }
838     /**
839      * Decrypt the message's session keys using either `decryptionKeys` or `passwords`.
840      * Either `armoredMessage` or `binaryMessage` must be given.
841      * @param options.armoredMessage - an armored message containing encrypted session key packets
842      * @param options.binaryMessage - a binary message containing encrypted session key packets
843      * @param options.date - date to use for key validity checks instead of the server time
844      * @throws if no session key could be found or decrypted
845      */
846     async decryptSessionKey({
847         decryptionKeys: decryptionKeyRefs = [],
848         armoredMessage,
849         binaryMessage,
850         ...options
851     }: WorkerDecryptionOptions) {
852         const decryptionKeys = toArray(decryptionKeyRefs).map(
853             (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
854         );
856         const message = await getMessage({ binaryMessage, armoredMessage });
858         const sessionKey = await decryptSessionKey({
859             ...options,
860             message,
861             decryptionKeys,
862         });
864         return sessionKey as SessionKeyWithoutPlaintextAlgo;
865     }
867     async processMIME({ verificationKeys: verificationKeyRefs = [], ...options }: WorkerProcessMIMEOptions) {
868         const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
869             this.keyStore.get(keyReference._idx)
870         );
872         const { signatures: signatureObjects, ...resultWithoutSignature } = await processMIME({
873             ...options,
874             verificationKeys,
875         });
877         const serialisedResult = {
878             ...resultWithoutSignature,
879             signatures: signatureObjects.map((sig) => sig.write() as Uint8Array),
880         };
881         return serialisedResult;
882     }
884     async getMessageInfo<DataType extends Data>({
885         armoredMessage,
886         binaryMessage,
887     }: WorkerGetMessageInfoOptions<DataType>): Promise<MessageInfo> {
888         const message = await getMessage({ binaryMessage, armoredMessage });
889         const signingKeyIDs = message.getSigningKeyIDs().map((keyID) => keyID.toHex());
890         const encryptionKeyIDs = message.getEncryptionKeyIDs().map((keyID) => keyID.toHex());
892         return { signingKeyIDs, encryptionKeyIDs };
893     }
895     async getSignatureInfo<DataType extends Data>({
896         armoredSignature,
897         binarySignature,
898     }: WorkerGetSignatureInfoOptions<DataType>): Promise<SignatureInfo> {
899         const signature = await getSignature({ binarySignature, armoredSignature });
900         const signingKeyIDs = signature.getSigningKeyIDs().map((keyID) => keyID.toHex());
902         return { signingKeyIDs };
903     }
905     /**
906      * Get basic info about a serialied key without importing it in the key store.
907      * E.g. determine whether the given key is private, and whether it is decrypted.
908      */
909     async getKeyInfo<T extends Data>({ armoredKey, binaryKey }: WorkerGetKeyInfoOptions<T>): Promise<KeyInfo> {
910         const key = await getKey({ binaryKey, armoredKey });
911         const keyIsPrivate = key.isPrivate();
912         const keyIsDecrypted = keyIsPrivate ? key.isDecrypted() : null;
913         const fingerprint = key.getFingerprint();
914         const keyIDs = key.getKeyIDs().map((keyID) => keyID.toHex());
916         return {
917             keyIsPrivate,
918             keyIsDecrypted,
919             fingerprint,
920             keyIDs,
921         };
922     }
924     /**
925      * Armor a message signature in binary form
926      */
927     async getArmoredSignature({ binarySignature }: { binarySignature: Uint8Array }) {
928         const signature = await getSignature({ binarySignature });
929         return signature.armor();
930     }
932     /**
933      * Armor a message given in binary form
934      */
935     async getArmoredMessage({ binaryMessage }: { binaryMessage: Uint8Array }) {
936         const armoredMessage = await armorBytes(binaryMessage);
937         return armoredMessage;
938     }
940     /**
941      * Given one or more keys concatenated in binary format, get the corresponding keys in armored format.
942      * The keys are not imported into the key store nor processed further. Both private and public keys are supported.
943      * @returns array of armored keys
944      */
945     async getArmoredKeys({ binaryKeys }: { binaryKeys: Uint8Array }) {
946         const keys = await readKeys({ binaryKeys });
947         return keys.map((key) => key.armor());
948     }
950     /**
951      * Returns whether the primary key is revoked.
952      * @param options.date - date to use for signature verification, instead of the server time
953      */
954     async isRevokedKey({ key: keyReference, date }: { key: KeyReference; date?: Date }) {
955         const key = this.keyStore.get(keyReference._idx);
956         const isRevoked = await isRevokedKey(key, date);
957         return isRevoked;
958     }
960     /**
961      * Returns whether the primary key is expired, or its creation time is in the future.
962      * @param options.date - date to use for the expiration check, instead of the server time
963      */
964     async isExpiredKey({ key: keyReference, date }: { key: KeyReference; date?: Date }) {
965         const key = this.keyStore.get(keyReference._idx);
966         const isExpired = await isExpiredKey(key, date);
967         return isExpired;
968     }
970     /**
971      * Check whether a key can successfully encrypt a message.
972      * This confirms that the key has encryption capabilities, it is neither expired nor revoked, and that its key material is valid.
973      */
974     async canKeyEncrypt({ key: keyReference, date }: { key: KeyReference; date?: Date }) {
975         const key = this.keyStore.get(keyReference._idx);
976         const canEncrypt = await canKeyEncrypt(key, date);
977         return canEncrypt;
978     }
980     async getSHA256Fingerprints({ key: keyReference }: { key: KeyReference }) {
981         const key = this.keyStore.get(keyReference._idx);
982         // this is quite slow since it hashes the key packets, even for v5 keys, instead of reusing the fingerprint.
983         // once v5 keys are more widespread and this function can be made more efficient, we could include `sha256Fingerprings` in `KeyReference` or `KeyInfo`.
984         const sha256Fingerprints = await getSHA256Fingerprints(key);
985         return sha256Fingerprints;
986     }
988     async computeHash({
989         algorithm,
990         data,
991     }: {
992         algorithm: 'unsafeMD5' | 'unsafeSHA1' | 'SHA512' | 'SHA256';
993         data: Uint8Array;
994     }) {
995         let hash;
996         switch (algorithm) {
997             case 'SHA512':
998                 hash = await SHA512(data);
999                 return hash;
1000             case 'SHA256':
1001                 hash = await SHA256(data);
1002                 return hash;
1003             case 'unsafeSHA1':
1004                 hash = await unsafeSHA1(data);
1005                 return hash;
1006             case 'unsafeMD5':
1007                 hash = await unsafeMD5(data);
1008                 return hash;
1009             default:
1010                 throw new Error(`Unsupported algorithm: ${algorithm}`);
1011         }
1012     }
1014     // this function may be merged with `computeHash` once we add streaming support to all/most hash algos
1015     async computeHashStream({ algorithm, dataStream }: ComputeHashStreamOptions) {
1016         let hashStream;
1017         switch (algorithm) {
1018             case 'unsafeSHA1':
1019                 hashStream = await unsafeSHA1(dataStream);
1020                 return hashStream;
1021             default:
1022                 throw new Error(`Unsupported algorithm: ${algorithm}`);
1023         }
1024     }
1026     /**
1027      * Compute argon2 key derivation of the given `password`
1028      */
1029     async computeArgon2({ password, salt, params = ARGON2_PARAMS.RECOMMENDED }: Argon2Options) {
1030         const result = await argon2({ password, salt, params });
1031         return result;
1032     }
1034     /**
1035      * Replace the User IDs of the target key to match those of the source key.
1036      * NOTE: this function mutates the target key in place, and does not update binding signatures.
1037      */
1038     async replaceUserIDs({
1039         sourceKey: sourceKeyReference,
1040         targetKey: targetKeyReference,
1041     }: {
1042         sourceKey: KeyReference;
1043         targetKey: PrivateKeyReference;
1044     }) {
1045         const sourceKey = this.keyStore.get(sourceKeyReference._idx);
1046         const targetKey = this.keyStore.get(targetKeyReference._idx);
1047         if (targetKey.getFingerprint() !== sourceKey.getFingerprint()) {
1048             throw new Error('Cannot replace UserIDs of a different key');
1049         }
1051         targetKey.users = sourceKey.users.map((sourceUser) => {
1052             // @ts-ignore missing .clone() definition
1053             const destUser = sourceUser.clone();
1054             destUser.mainKey = targetKey;
1055             return destUser;
1056         });
1057     }
1059     /**
1060      * Return a new key reference with changed userIDs.
1061      * Aside from the userIDs, the two keys are identical (e.g. same binding signatures).
1062      * The original key is not modified.
1063      */
1064     async cloneKeyAndChangeUserIDs({
1065         privateKey: privateKeyRef,
1066         userIDs,
1067     }: {
1068         privateKey: PrivateKeyReference;
1069         userIDs: MaybeArray<UserID>;
1070     }) {
1071         const originalKey = this.keyStore.get(privateKeyRef._idx) as PrivateKey;
1073         // @ts-ignore missing clone declaration
1074         const updatedKey: PrivateKey = originalKey.clone(true);
1076         // To preserve the original key signatures that are not involved with userIDs,
1077         // we first reformat the key to add & sign the new userIDs, then replace the userIDs of the original key.
1078         // To improve reformatting performance, we can drop subkeys beforehand, as they are not needed for the UserID
1079         const updatedSubkeys = updatedKey.subkeys;
1080         // NB: the private key params of the returned reformatted keys point to the same ones as `updatedKey`.
1081         // Hence, they will be cleared once the corresponding ref is cleared by the app -- no need to clear them now.
1082         const { publicKey: temporaryKeyWithNewUsers } = await reformatKey({
1083             privateKey: updatedKey,
1084             userIDs,
1085             format: 'object',
1086         });
1087         updatedKey.subkeys = updatedSubkeys;
1089         // same process as `updateUserIDs`
1090         updatedKey.users = temporaryKeyWithNewUsers.users.map((newUser) => {
1091             // @ts-ignore missing .clone() definition
1092             const destUser = newUser.clone();
1093             destUser.mainKey = updatedKey;
1094             return destUser;
1095         });
1097         const keyStoreID = this.keyStore.add(updatedKey);
1098         return getPrivateKeyReference(updatedKey, keyStoreID);
1099     }
1102 export interface ApiInterface extends Omit<Api, 'keyStore'> {}