diff --git a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts index 094a49fa718..b0a471cfdbd 100644 --- a/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts +++ b/extension/chrome/elements/compose-modules/compose-my-pubkey-module.ts @@ -3,16 +3,17 @@ 'use strict'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; -import { KeyInfo } from '../../../js/common/core/crypto/key.js'; +import { KeyInfo, KeyUtil } from '../../../js/common/core/crypto/key.js'; import { Lang } from '../../../js/common/lang.js'; import { Ui } from '../../../js/common/browser/ui.js'; import { ViewModule } from '../../../js/common/view-module.js'; import { ComposeView } from '../compose.js'; -import { ContactStore } from '../../../js/common/platform/store/contact-store.js'; +import { Str } from '../../../js/common/core/common.js'; export class ComposeMyPubkeyModule extends ViewModule { private toggledManually = false; + private wkdFingerprints: { [acctEmail: string]: string[] } = {}; public setHandlers = () => { this.view.S.cached('icon_pubkey').attr('title', Lang.compose.includePubkeyIconTitle); @@ -44,18 +45,40 @@ export class ComposeMyPubkeyModule extends ViewModule { return; } (async () => { - const contacts = await ContactStore.get(undefined, this.view.recipientsModule.getRecipients().map(r => r.email)); - for (const contact of contacts) { - if (contact?.has_pgp && contact.client !== 'cryptup') { + const senderEmail = this.view.senderModule.getSender(); + const senderKi = await this.view.storageModule.getKey(senderEmail); + const primaryFingerprint = (await KeyUtil.parse(senderKi.private)).id; + // if we have cashed this fingerprint, setAttachPreference(false) rightaway and return + const cached = this.wkdFingerprints[senderEmail]; + if (Array.isArray(cached) && cached.includes(primaryFingerprint)) { + this.setAttachPreference(false); + return; + } + const myDomain = Str.getDomainFromEmailAddress(senderEmail); + const foreignRecipients = this.view.recipientsModule.getRecipients().map(r => r.email) + .filter(Boolean) + .filter(email => myDomain !== Str.getDomainFromEmailAddress(email)); + if (foreignRecipients.length > 0) { + if (!Array.isArray(cached)) { + // slow operation -- test WKD for our own key and cache the result + const { keys } = await this.view.pubLookup.wkd.rawLookupEmail(senderEmail); + const fingerprints = keys.map(key => key.id); + this.wkdFingerprints[senderEmail] = fingerprints; + if (fingerprints.includes(primaryFingerprint)) { + this.setAttachPreference(false); + return; + } + } + for (const recipient of foreignRecipients) { // new message, and my key is not uploaded where the recipient would look for it - if (! await this.view.recipientsModule.doesRecipientHaveMyPubkey(contact.email)) { - // either don't know if they need pubkey (can_read_emails false), or they do need pubkey + if (! await this.view.recipientsModule.doesRecipientHaveMyPubkey(recipient)) { + // they do need pubkey this.setAttachPreference(true); return; } } + this.setAttachPreference(false); } - this.setAttachPreference(false); })().catch(ApiErr.reportIfSignificant); } diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index d94fc18fbf9..d23ce43063a 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -7,7 +7,7 @@ import { Contact } from '../../../js/common/core/crypto/key.js'; import { PUBKEY_LOOKUP_RESULT_FAIL, PUBKEY_LOOKUP_RESULT_WRONG } from './compose-err-module.js'; import { ProviderContactsQuery, Recipients } from '../../../js/common/api/email-provider/email-provider-api.js'; import { RecipientElement, RecipientStatus } from './compose-types.js'; -import { Str, Value } from '../../../js/common/core/common.js'; +import { Str } from '../../../js/common/core/common.js'; import { ApiErr } from '../../../js/common/api/shared/api-error.js'; import { Bm, BrowserMsg } from '../../../js/common/browser/browser-msg.js'; import { Catch } from '../../../js/common/platform/catch.js'; @@ -38,8 +38,6 @@ export class ComposeRecipientsModule extends ViewModule { private contactSearchInProgress = false; private addedPubkeyDbLookupInterval?: number; - private recipientsMissingMyKey: string[] = []; - private onRecipientAddedCallbacks: ((rec: RecipientElement[]) => void)[] = []; private dragged: Element | undefined = undefined; @@ -300,6 +298,7 @@ export class ComposeRecipientsModule extends ViewModule { this.onRecipientAddedCallbacks.push(callback); } + // todo: shouldn't we check longid? public doesRecipientHaveMyPubkey = async (theirEmailUnchecked: string): Promise => { const theirEmail = Str.parseEmail(theirEmailUnchecked).email; if (!theirEmail) { @@ -316,7 +315,7 @@ export class ComposeRecipientsModule extends ViewModule { const qReceivedMsg = `from:${theirEmail} "BEGIN PGP MESSAGE" "END PGP MESSAGE"`; try { const response = await this.view.emailProvider.msgList(`(${qSentPubkey}) OR (${qReceivedMsg})`, true); - if (response.messages) { + if (response.messages && response.messages.length > 0) { await AcctStore.set(this.view.acctEmail, { pubkey_sent_to: (storage.pubkey_sent_to || []).concat(theirEmail) }); return true; } else { @@ -857,7 +856,6 @@ export class ComposeRecipientsModule extends ViewModule { } private removeRecipient = (element: HTMLElement) => { - this.recipientsMissingMyKey = Value.arr.withoutVal(this.recipientsMissingMyKey, $(element).parent().text()); const index = this.addedRecipients.findIndex(r => r.element.isEqualNode(element)); this.addedRecipients[index].element.remove(); const container = element.parentElement?.parentElement; // Get Container, e.g. '.input-container-cc' diff --git a/extension/chrome/elements/compose.htm b/extension/chrome/elements/compose.htm index 05b02c25543..1713a5c640f 100644 --- a/extension/chrome/elements/compose.htm +++ b/extension/chrome/elements/compose.htm @@ -153,7 +153,7 @@

New Secure Message

- diff --git a/extension/js/common/api/email-provider/gmail/google-auth.ts b/extension/js/common/api/email-provider/gmail/google-auth.ts index 3e4aea1ad79..1a404dbe36c 100644 --- a/extension/js/common/api/email-provider/gmail/google-auth.ts +++ b/extension/js/common/api/email-provider/gmail/google-auth.ts @@ -6,7 +6,7 @@ // tslint:disable:oneliner-object-literal import { GOOGLE_API_HOST, GOOGLE_OAUTH_SCREEN_HOST, FLAVOR } from '../../../core/const.js'; -import { Url, Value } from '../../../core/common.js'; +import { Str, Url, Value } from '../../../core/common.js'; import { tabsQuery, windowsCreate } from '../../../browser/chrome.js'; import { Api } from './../../shared/api.js'; import { ApiErr } from '../../shared/api-error.js'; @@ -158,7 +158,7 @@ export class GoogleAuth { * Happens on enterprise builds */ public static isFesUnreachableErr = (e: any, email: string): boolean => { - const domain = email.split('@')[1].toLowerCase(); + const domain = Str.getDomainFromEmailAddress(email); const errString = String(e); if (errString.includes(`-1 when GET-ing https://${domain}/.well-known/host-meta.json`)) { return true; // err trying to get FES url from .well-known diff --git a/extension/js/common/api/key-server/wkd.ts b/extension/js/common/api/key-server/wkd.ts index c42fec84a9b..d2f3087e661 100644 --- a/extension/js/common/api/key-server/wkd.ts +++ b/extension/js/common/api/key-server/wkd.ts @@ -7,7 +7,8 @@ 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 { KeyUtil } from '../../core/crypto/key.js'; +import { Key, KeyUtil } from '../../core/crypto/key.js'; +import { Str } from '../../core/common.js'; // tslint:disable:no-null-keyword // tslint:disable:no-direct-ajax @@ -24,21 +25,23 @@ export class Wkd extends Api { super(); } - public lookupEmail = async (email: string): Promise => { + // returns all the received keys + public rawLookupEmail = async (email: string): Promise<{ keys: Key[], errs: Error[] }> => { + // todo: should we return errs on network failures etc.? const parts = email.split('@'); if (parts.length !== 2) { - return { pubkey: null, pgpClient: null }; + return { keys: [], errs: [] }; } const [user, recipientDomain] = parts; if (!user || !recipientDomain) { - return { pubkey: null, pgpClient: null }; + return { keys: [], errs: [] }; } if (!opgp) { // pgp_block.htm does not have openpgp loaded // the particular usecase (auto-loading pubkeys to verify signatures) is not that important, // the user typically gets the key loaded from composing anyway // the proper fix would be to run encodeZBase32 through background scripts - return { pubkey: null, pgpClient: null }; + return { keys: [], errs: [] }; } const directDomain = recipientDomain.toLowerCase(); const advancedDomainPrefix = (directDomain === 'localhost') ? '' : 'openpgpkey.'; @@ -50,15 +53,19 @@ export class Wkd extends Api { const directUrl = `https://${directHost}/.well-known/openpgpkey`; let response = await this.urlLookup(advancedUrl, userPart); if (!response.buf && response.hasPolicy) { - return { pubkey: null, pgpClient: null }; // do not retry direct if advanced had a policy file + return { keys: [], errs: [] }; // do not retry direct if advanced had a policy file } if (!response.buf) { response = await this.urlLookup(directUrl, userPart); } if (!response.buf) { - return { pubkey: null, pgpClient: null }; // do not retry direct if advanced had a policy file + return { keys: [], errs: [] }; // do not retry direct if advanced had a policy file } - const { keys, errs } = await KeyUtil.readMany(response.buf); + return await KeyUtil.readMany(response.buf); + } + + public lookupEmail = async (email: string): Promise => { + const { keys, errs } = await this.rawLookupEmail(email); if (errs.length) { return { pubkey: null, pgpClient: null }; } @@ -67,7 +74,7 @@ export class Wkd extends Api { return { pubkey: null, pgpClient: null }; } // if recipient uses same domain, we assume they use flowcrypt - const pgpClient = this.myOwnDomain === recipientDomain ? 'flowcrypt' : 'pgp-other'; + const pgpClient = this.myOwnDomain === Str.getDomainFromEmailAddress(email) ? 'flowcrypt' : 'pgp-other'; try { const pubkey = KeyUtil.armor(key); return { pubkey, pgpClient }; diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index f4490da8518..f7ac137307c 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -33,6 +33,11 @@ export class Str { return { email, name, full }; } + public static getDomainFromEmailAddress = (emailAddr: string) => { + // todo: parseEmail()? + return emailAddr.toLowerCase().split('@')[1]; + } + public static rmSpecialCharsKeepUtf = (str: string, mode: 'ALLOW-SOME' | 'ALLOW-NONE'): string => { // not a whitelist because we still want utf chars str = str.replace(/[@&#`();:'",<>\{\}\[\]\\\/\n\t\r]/gi, ''); diff --git a/extension/js/common/org-rules.ts b/extension/js/common/org-rules.ts index 0fcac3a057b..03a4171b045 100644 --- a/extension/js/common/org-rules.ts +++ b/extension/js/common/org-rules.ts @@ -33,7 +33,7 @@ export class OrgRules { throw new Error(`Not a valid email`); } const storage = await AcctStore.get(email, ['rules']); - return new OrgRules(storage.rules || OrgRules.default, acctEmail.split('@')[1]); + return new OrgRules(storage.rules || OrgRules.default, Str.getDomainFromEmailAddress(acctEmail)); } public static isPublicEmailProviderDomain = (emailAddrOrDomain: string) => { @@ -173,7 +173,7 @@ export class OrgRules { * This is because they already have other means to obtain public keys for these domains, such as from their own internal keyserver */ public canLookupThisRecipientOnAttester = (emailAddr: string): boolean => { - return !(this.domainRules.disallow_attester_search_for_domains || []).includes(emailAddr.split('@')[1] || 'NONE'); + return !(this.domainRules.disallow_attester_search_for_domains || []).includes(Str.getDomainFromEmailAddress(emailAddr) || 'NONE'); } /** diff --git a/test/source/mock/backend/backend-data.ts b/test/source/mock/backend/backend-data.ts index 7aec0edac82..5f354ead2e6 100644 --- a/test/source/mock/backend/backend-data.ts +++ b/test/source/mock/backend/backend-data.ts @@ -87,6 +87,9 @@ export class BackendData { "enforce_keygen_algo": "rsa2048", "disallow_attester_search_for_domains": [] }; + if (domain === 'google.mock.flowcryptlocal.com:8001') { + return { ...keyManagerAutogenRules, flags: [...keyManagerAutogenRules.flags, 'NO_ATTESTER_SUBMIT'] }; + } if (domain === 'key-manager-autogen.flowcrypt.com') { return keyManagerAutogenRules; } diff --git a/test/source/mock/key-manager/key-manager-endpoints.ts b/test/source/mock/key-manager/key-manager-endpoints.ts index aaf80c06084..de95d25818b 100644 --- a/test/source/mock/key-manager/key-manager-endpoints.ts +++ b/test/source/mock/key-manager/key-manager-endpoints.ts @@ -7,6 +7,7 @@ import { oauth } from '../lib/oauth'; import { Dict } from '../../core/common'; import { expect } from 'chai'; import { KeyUtil } from '../../core/crypto/key'; +import { wkdAtgooglemockflowcryptlocalcom8001Private } from '../../tests/tooling/consts'; // tslint:disable:max-line-length /* eslint-disable max-len */ @@ -193,6 +194,9 @@ export const mockKeyManagerEndpoints: HandlersDefinition = { '/flowcrypt-email-key-manager/keys/private': async ({ body }, req) => { const acctEmail = oauth.checkAuthorizationHeaderWithIdToken(req.headers.authorization); if (isGet(req)) { + if (acctEmail === 'wkd@google.mock.flowcryptlocal.com:8001') { + return { privateKeys: [{ decryptedPrivateKey: wkdAtgooglemockflowcryptlocalcom8001Private }] }; + } if (acctEmail === 'get.key@key-manager-autogen.flowcrypt.com') { return { privateKeys: [{ decryptedPrivateKey: existingPrv }] }; } diff --git a/test/source/mock/wkd/wkd-endpoints.ts b/test/source/mock/wkd/wkd-endpoints.ts index 7884be09b8b..5a264d20dbc 100644 --- a/test/source/mock/wkd/wkd-endpoints.ts +++ b/test/source/mock/wkd/wkd-endpoints.ts @@ -1,6 +1,8 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import { KeyUtil } from '../../core/crypto/key.js'; import { PgpArmor } from '../../core/crypto/pgp/pgp-armor.js'; +import { wkdAtgooglemockflowcryptlocalcom8001Private } from '../../tests/tooling/consts.js'; import { HandlersDefinition } from '../all-apis-mock'; const alice = `-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -190,6 +192,11 @@ ctnWuBzRDeI0n6XDaPv5TpKpS7uqy/fTlJLGE9vZTFUKzeGkQFomBoXNVWs= // todo - add a not found test with: throw new HttpClientErr('Pubkey not found', 404); export const mockWkdEndpoints: HandlersDefinition = { + '/.well-known/openpgpkey/hu/st5or5guodbnsiqbzp6i34xw59h1sgmw?l=wkd': async () => { + // direct for wkd@google.mock.flowcryptlocal.com:8001 + const pub = await KeyUtil.asPublicKey(await KeyUtil.parse(wkdAtgooglemockflowcryptlocalcom8001Private)); + return Buffer.from((await PgpArmor.dearmor(KeyUtil.armor(pub))).data); + }, '/.well-known/openpgpkey/hu/ihyath4noz8dsckzjbuyqnh4kbup6h4i?l=john.doe': async () => { return Buffer.from((await PgpArmor.dearmor(johnDoe1)).data); // direct for john.doe@localhost }, @@ -219,6 +226,6 @@ export const mockWkdEndpoints: HandlersDefinition = { return ''; // allow advanced for localhost }, '/.well-known/openpgpkey/policy': async () => { - return ''; // allow direct for localhost + return ''; // allow direct for all }, }; diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 04377256aa6..a970fff8d63 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -120,6 +120,37 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te expect(recipients).to.eq(['recip1@corp.co', 'recip2@corp.co', 'сс1@corp.co', 'bсс1@corp.co', '1 more'].join('')); })); + ava.default(`compose - auto include pubkey when our key is not available on Wkd`, testWithBrowser('ci.tests.gmail', async (t, browser) => { + const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compose'); + await composePage.page.setViewport({ width: 540, height: 606 }); + await ComposePageRecipe.fillMsg(composePage, { to: 'flowcrypt.compatibility@gmail.com' }, 'testing auto include pubkey'); + await composePage.waitTillGone('@spinner'); + await Util.sleep(3); // wait for the Wkd lookup to complete + expect(await composePage.hasClass('@action-include-pubkey', 'active')).to.be.false; + await composePage.waitAndType(`@input-to`, 'some.unknown@unknown.com'); + await composePage.waitAndFocus('@input-body'); + await composePage.waitTillGone('@spinner'); + await Util.sleep(3); // allow some time to search for messages + expect(await composePage.hasClass('@action-include-pubkey', 'active')).to.be.true; + })); + + ava.default(`compose - auto include pubkey is inactive when our key is available on Wkd`, testWithBrowser(undefined, async (t, browser) => { + const acct = 'wkd@google.mock.flowcryptlocal.com:8001'; + const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); + await SetupPageRecipe.autoKeygen(settingsPage); + const composePage = await ComposePageRecipe.openStandalone(t, browser, acct); + await composePage.page.setViewport({ width: 540, height: 606 }); + await ComposePageRecipe.fillMsg(composePage, { to: 'ci.tests.gmail@flowcrypt.dev' }, 'testing auto include pubkey'); + await composePage.waitTillGone('@spinner'); + await Util.sleep(3); // wait for the Wkd lookup to complete + expect(await composePage.hasClass('@action-include-pubkey', 'active')).to.be.false; + await composePage.waitAndType('@input-to', 'some.unknown@unknown.com'); + await composePage.waitAndFocus('@input-body'); + await composePage.waitTillGone('@spinner'); + await Util.sleep(3); // allow some time to search for messages + expect(await composePage.hasClass('@action-include-pubkey', 'active')).to.be.false; + })); + ava.default(`compose - freshly loaded pubkey`, testWithBrowser('ci.tests.gmail', async (t, browser) => { const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compose'); await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'freshly loaded pubkey'); diff --git a/test/source/tests/tooling/consts.ts b/test/source/tests/tooling/consts.ts index acdf1a7aac4..d8c25aaf510 100644 --- a/test/source/tests/tooling/consts.ts +++ b/test/source/tests/tooling/consts.ts @@ -368,4 +368,55 @@ QxptAWCHHIlAK9Q4VRJcvqxiwImhg55jORNB/MpywzNpH9rC4ffG6roCYQvryqmpmTvmNRZjCtso jz0glYBgSVo77aVfLvdtSXWsmsO9qCN/wII7UccKFGcu/wxqA/eWqiVle1oGlvbetswL8LtGtLTv lstiX22h/LfRGVPpuuH0cdxEMmEtTn7LcV3dK5Ynt8eiQdUN8akg3sO2rNbXRA== =WgF7 ------END PGP PRIVATE KEY BLOCK-----`; \ No newline at end of file +-----END PGP PRIVATE KEY BLOCK-----`; + +export const wkdAtgooglemockflowcryptlocalcom8001Private = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +ZcbMsfmsjQ2xERPJauMielisgbxBIIQs +=idfp +-----END PGP PRIVATE KEY BLOCK----- +`;