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';
13 checkKeyCompatibility,
18 doesKeySupportForwarding,
22 generateForwardingMaterial,
25 generateSessionKeyForAlgorithm,
26 getSHA256Fingerprints,
42 verifyCleartextMessage,
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';
52 ComputeHashStreamOptions,
59 SessionKey as SessionKeyWithoutPlaintextAlgo, // OpenPGP.js v5 has a 'plaintext' algo value for historical reasons.
61 WorkerDecryptionOptions,
63 WorkerEncryptSessionKeyOptions,
64 WorkerGenerateKeyOptions,
65 WorkerGenerateSessionKeyOptions,
66 WorkerGetKeyInfoOptions,
67 WorkerGetMessageInfoOptions,
68 WorkerGetSignatureInfoOptions,
69 WorkerImportPrivateKeyOptions,
70 WorkerImportPublicKeyOptions,
71 WorkerProcessMIMEOptions,
72 WorkerReformatKeyOptions,
74 WorkerVerifyCleartextOptions,
76 } from './api.models';
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 });
89 throw new Error('Must provide `armoredSignature` or `binarySignature`');
92 type SerializedMessageOptions = { armoredMessage?: string; binaryMessage?: Uint8Array };
93 const getMessage = async ({ armoredMessage, binaryMessage }: SerializedMessageOptions) => {
95 return readMessage({ armoredMessage });
96 } else if (binaryMessage) {
97 return readMessage({ binaryMessage });
99 throw new Error('Must provide `armoredMessage` or `binaryMessage`');
102 type SerializedKeyOptions = { armoredKey?: string; binaryKey?: Uint8Array };
103 const getKey = async ({ armoredKey, binaryKey }: SerializedKeyOptions) => {
105 return readKey({ armoredKey });
106 } else if (binaryKey) {
107 return readKey({ binaryKey });
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'] => {
120 return 'curve25519Legacy';
122 return 'ed25519Legacy';
133 switch (algorithmInfo.algorithm) {
136 algorithm: 'eddsaLegacy',
137 curve: 'ed25519Legacy',
142 curve: v5ToV6Curve(algorithmInfo.curve),
145 const result: AlgorithmInfo = { algorithm: algorithmInfo.algorithm };
146 if (algorithmInfo.curve !== undefined) {
147 result.curve = v5ToV6Curve(algorithmInfo.curve);
149 if (algorithmInfo.bits !== undefined) {
150 result.bits = algorithmInfo.bits;
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 = [];
173 keyContentHashNoCerts = await SHA256(publicKeyClone.write()).then(arrayToHexString);
175 keyContentHashNoCerts = keyContentHash;
180 checkKeyStrength(publicKey);
185 let compatibilityError: Error;
187 checkKeyCompatibility(publicKey);
189 compatibilityError = err;
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) =>
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();
212 getAlgorithmInfo: () => subkeyAlgoInfo,
213 getKeyID: () => subkeyKeyID,
216 } as PublicKeyReference;
219 const getPrivateKeyReference = async (privateKey: PrivateKey, keyStoreID: number): Promise<PrivateKeyReference> => {
220 const publicKeyReference = await getPublicKeyReference(privateKey.toPublic(), keyStoreID);
222 ...publicKeyReference,
223 isPrivate: () => true,
224 _dummyType: 'private',
225 } as PrivateKeyReference;
229 private store = new Map<number, Key>();
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.
237 private nextIdx = crypto.getRandomValues(new Uint32Array(1))[0];
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
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`);
252 this.store.set(idx, key);
253 this.nextIdx++; // increment regardless of customIdx, for code simplicity
258 const key = this.store.get(idx);
260 throw new Error('Key not found');
266 this.store.forEach((key) => {
267 if (key.isPrivate()) {
268 // @ts-ignore missing definition for clearPrivateParams()
269 key.clearPrivateParams();
273 // no need to reset index
277 const keyToClear = this.get(idx);
278 if (keyToClear.isPrivate()) {
279 // @ts-ignore missing definition for clearPrivateParams()
280 keyToClear.clearPrivateParams();
282 this.store.delete(idx);
286 type SerialisedOutputFormat = 'armored' | 'binary' | undefined;
287 type SerialisedOutputTypeFromFormat<F extends SerialisedOutputFormat> = F extends 'armored'
293 class KeyManagementApi {
294 protected keyStore = new KeyStore();
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.
300 async clearKeyStore() {
301 this.keyStore.clearAll();
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.
308 async clearKey({ key: keyReference }: { key: KeyReference }) {
309 this.keyStore.clear(keyReference._idx);
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
324 async generateKey(options: WorkerGenerateKeyOptions) {
325 const v6Tov5CurveOption = (curve: WorkerGenerateKeyOptions['curve']) => {
327 case 'ed25519Legacy':
328 case 'curve25519Legacy':
341 const { privateKey } = await generateKey({
343 curve: v6Tov5CurveOption(options.curve),
344 subkeys: options.subkeys?.map<SubkeyOptions>((subkeyOptions) => ({
346 curve: v6Tov5CurveOption(subkeyOptions.curve),
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()) {
353 'Unexpected "passphrase" option on key generation. Use "exportPrivateKey" after key generation to obtain a transferable encrypted key.'
356 const keyStoreID = this.keyStore.add(privateKey);
358 return getPrivateKeyReference(privateKey, keyStoreID);
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()) {
371 'Unexpected "passphrase" option on key reformat. Use "exportPrivateKey" after key reformatting to obtain a transferable encrypted key.'
374 const keyStoreID = this.keyStore.add(privateKey);
376 return getPrivateKeyReference(privateKey, keyStoreID);
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
388 async importPrivateKey<T extends Data>(
389 { armoredKey, binaryKey, passphrase, checkCompatibility }: WorkerImportPrivateKeyOptions<T>,
392 if (!armoredKey && !binaryKey) {
393 throw new Error('Must provide `armoredKey` or `binaryKey`');
395 const expectDecrypted = passphrase === null;
396 const maybeEncryptedKey = binaryKey
397 ? await readPrivateKey({ binaryKey })
398 : await readPrivateKey({ armoredKey: armoredKey! });
399 if (checkCompatibility) {
400 checkKeyCompatibility(maybeEncryptedKey);
403 if (expectDecrypted) {
404 if (!maybeEncryptedKey.isDecrypted()) {
405 throw new Error('Provide passphrase to import an encrypted private key');
407 decryptedKey = maybeEncryptedKey;
408 // @ts-ignore missing .validate() types
409 await decryptedKey.validate();
411 const usesArgon2 = maybeEncryptedKey.getKeys().some(
412 // @ts-ignore s2k field not declared
413 (keyOrSubkey) => keyOrSubkey.keyPacket.s2k && keyOrSubkey.keyPacket.s2k.type === 'argon2'
416 // TODO: Argon2 uses Wasm which requires special bundling
417 throw new Error('Keys encrypted using Argon2 are not supported yet');
419 decryptedKey = await decryptKey({ privateKey: maybeEncryptedKey, passphrase });
422 const keyStoreID = this.keyStore.add(decryptedKey, _customIdx);
424 return getPrivateKeyReference(decryptedKey, keyStoreID);
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
433 async importPublicKey<T extends Data>(
434 { armoredKey, binaryKey, checkCompatibility }: WorkerImportPublicKeyOptions<T>,
437 const publicKey = await getKey({ binaryKey, armoredKey });
438 if (checkCompatibility) {
439 checkKeyCompatibility(publicKey);
441 const keyStoreID = this.keyStore.add(publicKey, _customIdx);
442 return getPublicKeyReference(publicKey, keyStoreID);
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
451 async exportPublicKey<F extends SerialisedOutputFormat = 'armored'>({
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>;
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
471 async exportPrivateKey<F extends SerialisedOutputFormat = 'armored'>({
475 privateKey: PrivateKeyReference;
476 passphrase: string | null;
478 }): Promise<SerialisedOutputTypeFromFormat<F>> {
479 const { privateKey: keyReference, passphrase } = options;
480 if (!keyReference.isPrivate()) {
481 throw new Error('Private key expected');
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>;
493 * Each instance keeps a dedicated key storage.
495 export class Api extends KeyManagementApi {
497 * Init pmcrypto and set the underlying global OpenPGP config.
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
515 async encryptMessage<
516 DataType extends Data,
517 FormatType extends WorkerEncryptOptions<DataType>['format'] = 'armored',
518 DetachedType extends boolean = false,
520 encryptionKeys: encryptionKeyRefs = [],
521 signingKeys: signingKeyRefs = [],
527 }: WorkerEncryptOptions<DataType> & { format?: FormatType; detached?: DetachedType }) {
528 const signingKeys = toArray(signingKeyRefs).map(
529 (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
531 const encryptionKeys = toArray(encryptionKeyRefs).map(
532 (keyReference) => this.keyStore.get(keyReference._idx) as PublicKey
534 const inputSignature =
535 binarySignature || armoredSignature ? await getSignature({ armoredSignature, binarySignature }) : undefined;
537 if (config.preferredCompressionAlgorithm) {
539 'Passing `config.preferredCompressionAlgorithm` is not supported. Use `compress` option instead.'
543 const encryptionResult = await encryptMessage<DataType, FormatType, DetachedType>({
545 // @ts-ignore probably issue with mismatching underlying stream definitions
546 textData: options.textData,
549 signature: inputSignature,
552 preferredCompressionAlgorithm: compress ? enums.compression.zlib : enums.compression.uncompressed,
556 return encryptionResult;
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
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
579 const signResult = await signMessage<DataType, FormatType, boolean>({
581 // @ts-ignore probably issue with mismatching underlying stream definitions
582 textData: options.textData,
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
603 async verifyMessage<DataType extends Data, FormatType extends WorkerVerifyOptions<DataType>['format'] = 'utf8'>({
606 verificationKeys: verificationKeyRefs = [],
608 }: WorkerVerifyOptions<DataType> & { format?: FormatType }) {
609 const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
610 this.keyStore.get(keyReference._idx)
612 const signature = await getSignature({ armoredSignature, binarySignature });
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
623 return serialisedResult;
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
632 async verifyCleartextMessage({
633 armoredCleartextMessage,
634 verificationKeys: verificationKeyRefs = [],
636 }: WorkerVerifyCleartextOptions) {
637 const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
638 this.keyStore.get(keyReference._idx)
640 const cleartextMessage = await readCleartextMessage({ cleartextMessage: armoredCleartextMessage });
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
651 return serialisedResult;
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
667 async decryptMessage<FormatType extends WorkerDecryptionOptions['format'] = 'utf8'>({
668 decryptionKeys: decryptionKeyRefs = [],
669 verificationKeys: verificationKeyRefs = [],
674 armoredEncryptedSignature: armoredEncSignature,
675 binaryEncryptedSignature: binaryEncSingature,
677 }: WorkerDecryptionOptions & { format?: FormatType }) {
678 const decryptionKeys = toArray(decryptionKeyRefs).map(
679 (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
681 const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
682 this.keyStore.get(keyReference._idx)
685 const message = await getMessage({ binaryMessage, armoredMessage });
687 binarySignature || armoredSignature ? await getSignature({ binarySignature, armoredSignature }) : undefined;
688 const encryptedSignature =
689 binaryEncSingature || armoredEncSignature
690 ? await getMessage({ binaryMessage: binaryEncSingature, armoredMessage: armoredEncSignature })
693 const { signatures: signatureObjects, ...decryptionResultWithoutSignatures } = await decryptMessage<
705 const serialisedResult = {
706 ...decryptionResultWithoutSignatures,
707 signatures: signatureObjects.map((sig) => sig.write() as Uint8Array), // no support for streamed input for now
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
720 * Generate forwardee key and proxy parameter needed to setup end-to-end encrypted forwarding for the given
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
727 async generateE2EEForwardingMaterial({
729 userIDsForForwardeeKey,
733 forwarderKey: PrivateKeyReference;
734 userIDsForForwardeeKey: MaybeArray<UserID>;
735 passphrase: string | null;
738 const originalKey = this.keyStore.get(forwarderKey._idx) as PrivateKey;
740 const { proxyInstances, forwardeeKey } = await generateForwardingMaterial(
742 userIDsForForwardeeKey,
746 const maybeEncryptedKey = passphrase
747 ? await encryptKey({ privateKey: forwardeeKey, passphrase })
751 forwardeeKey: maybeEncryptedKey.armor(),
757 * Check whether a key can be used as input to `generateE2EEForwardingMaterial` to setup E2EE forwarding.
759 async doesKeySupportE2EEForwarding({
760 forwarderKey: keyReference,
763 forwarderKey: PrivateKeyReference;
766 const key = this.keyStore.get(keyReference._idx);
767 if (!key.isPrivate()) {
770 const supportsForwarding = await doesKeySupportForwarding(key, date);
771 return supportsForwarding;
775 * Whether a key is a E2EE forwarding recipient key, where all its encryption-capable (sub)keys are setup
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
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');
789 const forForwarding = await isForwardingKey(key, date);
790 return forForwarding;
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.
798 async generateSessionKeyForAlgorithm(algoName: Parameters<typeof generateSessionKeyForAlgorithm>[0]) {
799 const sessionKeyBytes = await generateSessionKeyForAlgorithm(algoName);
800 return sessionKeyBytes;
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.
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;
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
823 async encryptSessionKey<FormatType extends WorkerEncryptSessionKeyOptions['format'] = 'armored'>({
824 encryptionKeys: encryptionKeyRefs = [],
826 }: WorkerEncryptSessionKeyOptions & { format?: FormatType }): Promise<SerialisedOutputTypeFromFormat<FormatType>> {
827 const encryptionKeys = toArray(encryptionKeyRefs).map(
828 (keyReference) => this.keyStore.get(keyReference._idx) as PublicKey
830 const encryptedData = await encryptSessionKey<FormatType>({
835 return encryptedData as SerialisedOutputTypeFromFormat<FormatType>;
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
846 async decryptSessionKey({
847 decryptionKeys: decryptionKeyRefs = [],
851 }: WorkerDecryptionOptions) {
852 const decryptionKeys = toArray(decryptionKeyRefs).map(
853 (keyReference) => this.keyStore.get(keyReference._idx) as PrivateKey
856 const message = await getMessage({ binaryMessage, armoredMessage });
858 const sessionKey = await decryptSessionKey({
864 return sessionKey as SessionKeyWithoutPlaintextAlgo;
867 async processMIME({ verificationKeys: verificationKeyRefs = [], ...options }: WorkerProcessMIMEOptions) {
868 const verificationKeys = toArray(verificationKeyRefs).map((keyReference) =>
869 this.keyStore.get(keyReference._idx)
872 const { signatures: signatureObjects, ...resultWithoutSignature } = await processMIME({
877 const serialisedResult = {
878 ...resultWithoutSignature,
879 signatures: signatureObjects.map((sig) => sig.write() as Uint8Array),
881 return serialisedResult;
884 async getMessageInfo<DataType extends Data>({
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 };
895 async getSignatureInfo<DataType extends Data>({
898 }: WorkerGetSignatureInfoOptions<DataType>): Promise<SignatureInfo> {
899 const signature = await getSignature({ binarySignature, armoredSignature });
900 const signingKeyIDs = signature.getSigningKeyIDs().map((keyID) => keyID.toHex());
902 return { signingKeyIDs };
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.
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());
925 * Armor a message signature in binary form
927 async getArmoredSignature({ binarySignature }: { binarySignature: Uint8Array }) {
928 const signature = await getSignature({ binarySignature });
929 return signature.armor();
933 * Armor a message given in binary form
935 async getArmoredMessage({ binaryMessage }: { binaryMessage: Uint8Array }) {
936 const armoredMessage = await armorBytes(binaryMessage);
937 return armoredMessage;
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
945 async getArmoredKeys({ binaryKeys }: { binaryKeys: Uint8Array }) {
946 const keys = await readKeys({ binaryKeys });
947 return keys.map((key) => key.armor());
951 * Returns whether the primary key is revoked.
952 * @param options.date - date to use for signature verification, instead of the server time
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);
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
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);
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.
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);
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;
992 algorithm: 'unsafeMD5' | 'unsafeSHA1' | 'SHA512' | 'SHA256';
998 hash = await SHA512(data);
1001 hash = await SHA256(data);
1004 hash = await unsafeSHA1(data);
1007 hash = await unsafeMD5(data);
1010 throw new Error(`Unsupported algorithm: ${algorithm}`);
1014 // this function may be merged with `computeHash` once we add streaming support to all/most hash algos
1015 async computeHashStream({ algorithm, dataStream }: ComputeHashStreamOptions) {
1017 switch (algorithm) {
1019 hashStream = await unsafeSHA1(dataStream);
1022 throw new Error(`Unsupported algorithm: ${algorithm}`);
1027 * Compute argon2 key derivation of the given `password`
1029 async computeArgon2({ password, salt, params = ARGON2_PARAMS.RECOMMENDED }: Argon2Options) {
1030 const result = await argon2({ password, salt, params });
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.
1038 async replaceUserIDs({
1039 sourceKey: sourceKeyReference,
1040 targetKey: targetKeyReference,
1042 sourceKey: KeyReference;
1043 targetKey: PrivateKeyReference;
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');
1051 targetKey.users = sourceKey.users.map((sourceUser) => {
1052 // @ts-ignore missing .clone() definition
1053 const destUser = sourceUser.clone();
1054 destUser.mainKey = targetKey;
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.
1064 async cloneKeyAndChangeUserIDs({
1065 privateKey: privateKeyRef,
1068 privateKey: PrivateKeyReference;
1069 userIDs: MaybeArray<UserID>;
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,
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;
1097 const keyStoreID = this.keyStore.add(updatedKey);
1098 return getPrivateKeyReference(updatedKey, keyStoreID);
1102 export interface ApiInterface extends Omit<Api, 'keyStore'> {}