diff --git a/extension/chrome/elements/compose-modules/compose-storage-module.ts b/extension/chrome/elements/compose-modules/compose-storage-module.ts index 76cd8560714..b095afb27d9 100644 --- a/extension/chrome/elements/compose-modules/compose-storage-module.ts +++ b/extension/chrome/elements/compose-modules/compose-storage-module.ts @@ -14,7 +14,7 @@ import { ComposeView } from '../compose.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { GlobalStore } from '../../../js/common/platform/store/global-store.js'; -import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; +import { ContactStore, ContactUpdate } from '../../../js/common/platform/store/contact-store.js'; import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js'; import { Settings } from '../../../js/common/settings.js'; import { Ui } from '../../../js/common/browser/ui.js'; @@ -102,20 +102,36 @@ export class ComposeStorageModule extends ViewModule { try { const lookupResult = await this.view.pubLookup.lookupEmail(email); if (lookupResult && email) { - if (lookupResult.pubkey) { - const key = await KeyUtil.parse(lookupResult.pubkey); - if (!key.usableForEncryption && !KeyUtil.expired(key)) { // Not to skip expired keys + const pubkeys: Key[] = []; + for (const pubkey of lookupResult.pubkeys) { + const key = await KeyUtil.parse(pubkey); + if (!key.usableForEncryption && !key.revoked && !KeyUtil.expired(key)) { // Not to skip expired and revoked keys console.info('Dropping found+parsed key because getEncryptionKeyPacket===null', { for: email, fingerprint: key.id }); Ui.toast(`Public Key retrieved for email ${email} with id ${key.id} was ignored because it's not usable for encryption.`, 5); - lookupResult.pubkey = null; // tslint:disable-line:no-null-keyword + } else { + pubkeys.push(key); } } - const ksContact = await ContactStore.obj({ email, name, pubkey: lookupResult.pubkey, lastCheck: Date.now() }); - if (ksContact.pubkey) { - this.ksLookupsByEmail[email] = ksContact.pubkey; + // save multiple pubkeys as separate operations + // todo: add a convenient method to storage? + const updates: ContactUpdate[] = []; + if (!pubkeys.length) { + if (name) { + // update just name + updates.push({ name } as ContactUpdate); + } else { + // No public key found. Returning early, nothing to update in local store below. + return await ContactStore.obj({ email }); + } + } + for (const pubkey of pubkeys) { + updates.push({ name, pubkey, pubkeyLastCheck: Date.now() }); + } + if (updates.length) { + await Promise.all(updates.map(async (update) => await ContactStore.update(undefined, email, update))); } - await ContactStore.save(undefined, ksContact); - return ksContact; + const [preferred] = await ContactStore.get(undefined, [email]); + return preferred ?? PUBKEY_LOOKUP_RESULT_FAIL; } else { return PUBKEY_LOOKUP_RESULT_FAIL; } diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts index f737a49816d..06f25efbfab 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-signature-module.ts @@ -9,7 +9,6 @@ import { PgpBlockView } from '../pgp_block'; import { Ui } from '../../../js/common/browser/ui.js'; import { VerifyRes } from '../../../js/common/core/crypto/pgp/msg-util.js'; import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; -import { OpenPGPKey } from '../../../js/common/core/crypto/pgp/openpgp-key.js'; import { Str } from '../../../js/common/core/common.js'; export class PgpBlockViewSignatureModule { @@ -64,19 +63,19 @@ export class PgpBlockViewSignatureModule { return; } // ---> and user doesn't have pubkey for that email addr - const { pubkey } = await this.view.pubLookup.lookupEmail(senderEmail); - if (!pubkey) { + const { pubkeys } = await this.view.pubLookup.lookupEmail(senderEmail); + if (!pubkeys.length) { render(`Missing pubkey ${signerLongid}`, () => undefined); return; } // ---> and pubkey found on keyserver by sender email - const { key } = await BrowserMsg.send.bg.await.keyParse({ armored: pubkey }); - if (!key.allIds.map(id => OpenPGPKey.fingerprintToLongid(id)).includes(signerLongid)) { - render(`Fetched sender's pubkey ${OpenPGPKey.fingerprintToLongid(key.id)} but message was signed with a different key: ${signerLongid}, will not verify.`, () => undefined); + const { key: pubkey } = await BrowserMsg.send.bg.await.keyMatch({ pubkeys, longid: signerLongid }); + if (!pubkey) { + render(`Fetched ${pubkeys.length} sender's pubkeys but message was signed with a different key: ${signerLongid}, will not verify.`, () => undefined); return; } // ---> and longid it matches signature - await ContactStore.save(undefined, await ContactStore.obj({ email: senderEmail, pubkey })); // <= TOFU auto-import + await ContactStore.update(undefined, senderEmail, { pubkey }); // <= TOFU auto-import render('Fetched pubkey, click to verify', () => window.location.reload()); } else { // don't know who sent it render('Cannot verify: missing pubkey, missing sender info', () => undefined); diff --git a/extension/chrome/settings/setup.ts b/extension/chrome/settings/setup.ts index 45f38f21888..2660701cbce 100644 --- a/extension/chrome/settings/setup.ts +++ b/extension/chrome/settings/setup.ts @@ -62,7 +62,6 @@ export class SetupView extends View { public pubLookup!: PubLookup; public keyManager: KeyManager | undefined; // not set if no url in org rules - public acctEmailAttesterPubId: string | undefined; public fetchedKeyBackups: KeyInfo[] = []; public fetchedKeyBackupsUniqueLongids: string[] = []; public importedKeysUniqueLongids: string[] = []; diff --git a/extension/chrome/settings/setup/setup-render.ts b/extension/chrome/settings/setup/setup-render.ts index bdf108ed828..23a749a67f3 100644 --- a/extension/chrome/settings/setup/setup-render.ts +++ b/extension/chrome/settings/setup/setup-render.ts @@ -8,7 +8,6 @@ import { Settings } from '../../../js/common/settings.js'; import { SetupView } from '../setup.js'; import { AcctStore } from '../../../js/common/platform/store/acct-store.js'; import { KeyStore } from '../../../js/common/platform/store/key-store.js'; -import { KeyUtil } from '../../../js/common/core/crypto/key.js'; export class SetupRenderModule { @@ -97,9 +96,7 @@ export class SetupRenderModule { } catch (e) { return await Settings.promptToRetry(e, Lang.setup.failedToCheckIfAcctUsesEncryption, () => this.renderSetupDialog()); } - if (keyserverRes.pubkey) { - const pub = await KeyUtil.parse(keyserverRes.pubkey); - this.view.acctEmailAttesterPubId = pub.id; + if (keyserverRes.pubkeys.length) { if (!this.view.orgRules.canBackupKeys()) { // they already have a key recorded on attester, but no backups allowed on the domain. They should enter their prv manually this.displayBlock('step_2b_manual_enter'); diff --git a/extension/js/common/api/key-server/wkd.ts b/extension/js/common/api/key-server/wkd.ts index aaac133d33c..66dea82cfb6 100644 --- a/extension/js/common/api/key-server/wkd.ts +++ b/extension/js/common/api/key-server/wkd.ts @@ -6,10 +6,9 @@ import { Api } from './../shared/api.js'; import { ApiErr } from '../shared/api-error.js'; import { opgp } from '../../core/crypto/pgp/openpgpjs-custom.js'; import { Buf } from '../../core/buf.js'; -import { PubkeySearchResult } from './../pub-lookup.js'; +import { PubkeysSearchResult } from './../pub-lookup.js'; import { Key, KeyUtil } from '../../core/crypto/key.js'; -// tslint:disable:no-null-keyword // tslint:disable:no-direct-ajax export class Wkd extends Api { @@ -59,20 +58,19 @@ export class Wkd extends Api { return await KeyUtil.readMany(response.buf); } - public lookupEmail = async (email: string): Promise => { + public lookupEmail = async (email: string): Promise => { const { keys, errs } = await this.rawLookupEmail(email); if (errs.length) { - return { pubkey: null }; + return { pubkeys: [] }; } - const key = keys.find(key => key.usableForEncryption && key.emails.some(x => x.toLowerCase() === email.toLowerCase())); - if (!key) { - return { pubkey: null }; + const pubkeys = keys.filter(key => key.emails.some(x => x.toLowerCase() === email.toLowerCase())); + if (!pubkeys.length) { + return { pubkeys: [] }; } try { - const pubkey = KeyUtil.armor(key); - return { pubkey }; + return { pubkeys: pubkeys.map(pubkey => KeyUtil.armor(pubkey)) }; } catch (e) { - return { pubkey: null }; + return { pubkeys: [] }; } } diff --git a/extension/js/common/api/pub-lookup.ts b/extension/js/common/api/pub-lookup.ts index d85fc01841a..29b36ed7c4a 100644 --- a/extension/js/common/api/pub-lookup.ts +++ b/extension/js/common/api/pub-lookup.ts @@ -9,6 +9,7 @@ import { Wkd } from './key-server/wkd.js'; import { OrgRules } from '../org-rules.js'; export type PubkeySearchResult = { pubkey: string | null }; +export type PubkeysSearchResult = { pubkeys: string[] }; /** * Look up public keys. @@ -38,24 +39,28 @@ export class PubLookup { } } - public lookupEmail = async (email: string): Promise => { + public lookupEmail = async (email: string): Promise => { if (this.keyManager) { const res = await this.keyManager.lookupPublicKey(email); if (res.publicKeys.length) { - return { pubkey: res.publicKeys[0].publicKey }; + return { pubkeys: res.publicKeys.map(x => x.publicKey) }; } } const wkdRes = await this.wkd.lookupEmail(email); - if (wkdRes.pubkey) { + if (wkdRes.pubkeys.length) { return wkdRes; } if (this.internalSks) { const res = await this.internalSks.lookupEmail(email); if (res.pubkey) { - return res; + return { pubkeys: [res.pubkey] }; } } - return await this.attester.lookupEmail(email); + const attRes = await this.attester.lookupEmail(email); + if (attRes.pubkey) { + return { pubkeys: [attRes.pubkey] }; + } + return { pubkeys: [] }; } public lookupFingerprint = async (fingerprintOrLongid: string): Promise => { diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 1ca9e1e0c56..c39f10af1fb 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -18,6 +18,7 @@ import { Ui } from './ui.js'; import { GlobalStoreDict, GlobalIndex } from '../platform/store/global-store.js'; import { AcctStoreDict, AccountIndex } from '../platform/store/acct-store.js'; import { Contact, Key, KeyUtil } from '../core/crypto/key.js'; +import { OpenPGPKey } from '../core/crypto/pgp/openpgp-key.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; @@ -62,6 +63,7 @@ export namespace Bm { export type PgpHashChallengeAnswer = { answer: string }; export type PgpMsgType = PgpMsgMethod.Arg.Type; export type KeyParse = { armored: string }; + export type KeyMatch = { pubkeys: string[], longid: string }; export type Ajax = { req: JQueryAjaxSettings, stack: string }; export type AjaxGmailAttachmentGetChunk = { acctEmail: string, msgId: string, attachmentId: string }; export type ShowAttachmentPreview = { iframeUrl: string }; @@ -82,6 +84,7 @@ export namespace Bm { export type PgpMsgType = PgpMsgTypeResult; export type PgpHashChallengeAnswer = { hashed: string }; export type KeyParse = { key: Key }; + export type KeyMatch = { key: Key | undefined }; export type AjaxGmailAttachmentGetChunk = { chunk: Buf }; export type _tab_ = { tabId: string | null | undefined }; export type Db = any; // not included in Any below @@ -90,7 +93,7 @@ export namespace Bm { export type Any = GetActiveTabInfo | _tab_ | ReconnectAcctAuthPopup | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerify | PgpHashChallengeAnswer | PgpMsgType | KeyParse | StoreSessionGet | StoreSessionSet | StoreAcctGet | StoreAcctSet | StoreGlobalGet | StoreGlobalSet - | AjaxGmailAttachmentGetChunk; + | AjaxGmailAttachmentGetChunk | KeyMatch; } export type AnyRequest = PassphraseEntry | StripeResult | OpenPage | OpenGoogleAuthDialog | Redirect | Reload | @@ -99,7 +102,7 @@ export namespace Bm { NotificationShow | PassphraseDialog | PassphraseDialog | Settings | SetCss | AddOrRemoveClass | ReconnectAcctAuthPopup | Db | StoreSessionSet | StoreSessionGet | StoreGlobalGet | StoreGlobalSet | StoreAcctGet | StoreAcctSet | KeyParse | PgpMsgDecrypt | PgpMsgDiagnoseMsgPubkeys | PgpMsgVerifyDetached | PgpHashChallengeAnswer | PgpMsgType | Ajax | FocusFrame | - ShowAttachmentPreview | ReRenderRecipient; + ShowAttachmentPreview | ReRenderRecipient | KeyMatch; // export type RawResponselessHandler = (req: AnyRequest) => Promise; // export type RawRespoHandler = (req: AnyRequest) => Promise; @@ -146,6 +149,7 @@ export class BrowserMsg { pgpMsgDecrypt: (bm: Bm.PgpMsgDecrypt) => BrowserMsg.sendAwait(undefined, 'pgpMsgDecrypt', bm, true) as Promise, pgpMsgVerifyDetached: (bm: Bm.PgpMsgVerifyDetached) => BrowserMsg.sendAwait(undefined, 'pgpMsgVerifyDetached', bm, true) as Promise, keyParse: (bm: Bm.KeyParse) => BrowserMsg.sendAwait(undefined, 'keyParse', bm, true) as Promise, + keyMatch: (bm: Bm.KeyMatch) => BrowserMsg.sendAwait(undefined, 'keyMatch', bm, true) as Promise, pgpMsgType: (bm: Bm.PgpMsgType) => BrowserMsg.sendAwait(undefined, 'pgpMsgType', bm, true) as Promise, }, }, @@ -244,6 +248,11 @@ export class BrowserMsg { BrowserMsg.bgAddListener('pgpMsgVerifyDetached', MsgUtil.verifyDetached); BrowserMsg.bgAddListener('pgpMsgType', MsgUtil.type); BrowserMsg.bgAddListener('keyParse', async (r: Bm.KeyParse) => ({ key: await KeyUtil.parse(r.armored) })); + BrowserMsg.bgAddListener('keyMatch', async (r: Bm.KeyMatch) => ({ + key: + (await Promise.all(r.pubkeys.map(async (pub) => await KeyUtil.parse(pub)))). + find(k => k.allIds.map(id => OpenPGPKey.fingerprintToLongid(id).includes(r.longid))) + })); } public static addListener = (name: string, handler: Handler) => { diff --git a/extension/js/common/core/crypto/key.ts b/extension/js/common/core/crypto/key.ts index e25eaabc62b..00e84e48cbb 100644 --- a/extension/js/common/core/crypto/key.ts +++ b/extension/js/common/core/crypto/key.ts @@ -23,6 +23,7 @@ export interface Key { id: string; // a fingerprint of the primary key in OpenPGP, and similarly a fingerprint of the actual cryptographic key (eg RSA fingerprint) in S/MIME allIds: string[]; // a list of fingerprints, including those for subkeys created: number; + revoked: boolean; lastModified: number | undefined; // date of last signature, or undefined if never had valid signature expiration: number | undefined; // number of millis of expiration or undefined if never expires usableForEncryption: boolean; diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index c365d53d6b2..c0e72c7e70d 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -232,6 +232,7 @@ export class OpenPGPKey { curve: (algoInfo as any).curve as string | undefined, algorithmId: opgp.enums.publicKey[algoInfo.algorithm] }, + revoked: keyWithoutWeakPackets.revocationSignatures.length > 0 } as Key); (key as any)[internal] = keyWithoutWeakPackets; (key as any).rawKey = opgpKey; diff --git a/extension/js/common/core/crypto/smime/smime-key.ts b/extension/js/common/core/crypto/smime/smime-key.ts index bcbbaf735dc..8fe1ab50f8f 100644 --- a/extension/js/common/core/crypto/smime/smime-key.ts +++ b/extension/js/common/core/crypto/smime/smime-key.ts @@ -119,6 +119,7 @@ export class SmimeKey { fullyEncrypted: false, isPublic: !certificate.privateKey, isPrivate: !!certificate.privateKey, + revoked: false, // todo: issuerAndSerialNumber: forge.asn1.toDer(issuerAndSerialNumberAsn1).getBytes() } as Key; (key as unknown as { rawArmored: string }).rawArmored = pem; diff --git a/extension/js/content_scripts/webmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail-element-replacer.ts index fd5aa20600e..16e3d2e7079 100644 --- a/extension/js/content_scripts/webmail/gmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/gmail-element-replacer.ts @@ -693,7 +693,7 @@ export class GmailElementReplacer implements WebmailElementReplacer { const [contact] = await ContactStore.get(undefined, [email]); if (contact && contact.pubkey) { this.recipientHasPgpCache[email] = true; - } else if ((await this.pubLookup.lookupEmail(email)).pubkey) { + } else if ((await this.pubLookup.lookupEmail(email)).pubkeys.length) { this.recipientHasPgpCache[email] = true; } else { this.recipientHasPgpCache[email] = false; diff --git a/test/source/tests/browser-unit-tests/unit-Wkd.js b/test/source/tests/browser-unit-tests/unit-Wkd.js index e0afbc5ada2..66c1d10528f 100644 --- a/test/source/tests/browser-unit-tests/unit-Wkd.js +++ b/test/source/tests/browser-unit-tests/unit-Wkd.js @@ -26,11 +26,11 @@ BROWSER_UNIT_TEST_NAME(`Wkd direct method`); wkd.port = 8001; let email; email = 'john.doe@localhost'; - if (!(await wkd.lookupEmail(email)).pubkey) { + if (!(await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } email = 'John.Doe@localhost'; - if (!(await wkd.lookupEmail(email)).pubkey) { + if (!(await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } return 'pass'; @@ -42,30 +42,32 @@ BROWSER_UNIT_TEST_NAME(`Wkd advanced method`); wkd.port = 8001; let email; email = 'john.doe@localhost'; - if (!(await wkd.lookupEmail(email)).pubkey) { + if (!(await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } email = 'John.Doe@localHOST'; - if (!(await wkd.lookupEmail(email)).pubkey) { + if (!(await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } return 'pass'; })(); -BROWSER_UNIT_TEST_NAME(`Wkd client picks valid key among revoked keys`); +BROWSER_UNIT_TEST_NAME(`Wkd client returns all keys`); (async () => { const wkd = new Wkd(); wkd.port = 8001; const email = 'some.revoked@localhost'; - const pubkey = (await wkd.lookupEmail(email)).pubkey; - if (!pubkey) { + const pubkeys = (await wkd.lookupEmail(email)).pubkeys; + if (!pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } - const key = await KeyUtil.parse(pubkey); - if (key && key.id.toUpperCase() === 'D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2' && key.usableForEncryption) { + const ids = (await Promise.all(pubkeys.map(async(pubkey) => await KeyUtil.parse(pubkey)))).map(key => key.id.toUpperCase()); + if (ids.length === 3 && ids.includes('D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2') && + ids.includes('A5CFC8E8EA4AE69989FE2631097EEBF354259A5E') && + ids.includes('3930752556D57C46A1C56B63DE8538DDA1648C76')) { return 'pass'; } else { - return `Expected key with id=D6662C5FB9BDE9DA01F3994AAA1EF832D8CCA4F2 wasn't received`; + return "Expected keys weren't received"; } })(); @@ -74,7 +76,7 @@ BROWSER_UNIT_TEST_NAME(`Wkd advanced shouldn't fall back on direct if advanced p const wkd = new Wkd(); wkd.port = 8001; const email = 'jack.advanced@localhost'; - if ((await wkd.lookupEmail(email)).pubkey) { + if ((await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't expect a pubkey`); } return 'pass'; @@ -85,7 +87,7 @@ BROWSER_UNIT_TEST_NAME(`Wkd incorrect UID should fail`); const wkd = new Wkd(); wkd.port = 8001; const email = 'incorrect@localhost'; - if ((await wkd.lookupEmail(email)).pubkey) { + if ((await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't expect a pubkey`); } return 'pass'; @@ -95,7 +97,7 @@ BROWSER_UNIT_TEST_NAME(`Wkd should extract key for human@flowcrypt.com`); (async () => { const wkd = new Wkd(); const email = 'human@flowcrypt.com'; - if (!(await wkd.lookupEmail(email)).pubkey) { + if (!(await wkd.lookupEmail(email)).pubkeys.length) { throw Error(`Wkd for ${email} didn't return a pubkey`); } return 'pass';