From f5710c8a168bfffef35c81721e31cc5788486a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 14:01:30 +0000 Subject: [PATCH 1/7] build(deps-dev): bump @openpgp/web-stream-tools from 0.0.11 to 0.0.13 Bumps [@openpgp/web-stream-tools](https://github.com/openpgpjs/web-stream-tools) from 0.0.11 to 0.0.13. - [Release notes](https://github.com/openpgpjs/web-stream-tools/releases) - [Commits](https://github.com/openpgpjs/web-stream-tools/compare/v0.0.11...v0.0.13) --- updated-dependencies: - dependency-name: "@openpgp/web-stream-tools" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 27 +++++++++++---------------- package.json | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6317bebf0d..3c26e5f2deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "zxcvbn": "4.4.2" }, "devDependencies": { - "@openpgp/web-stream-tools": "^0.0.11", + "@openpgp/web-stream-tools": "^0.0.13", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", "@types/chrome": "0.0.212", @@ -323,15 +323,6 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@mattiasbuelens/web-streams-adapter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz", - "integrity": "sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/@mdn/browser-compat-data": { "version": "5.2.29", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.2.29.tgz", @@ -374,13 +365,17 @@ } }, "node_modules/@openpgp/web-stream-tools": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@openpgp/web-stream-tools/-/web-stream-tools-0.0.11.tgz", - "integrity": "sha512-52NMPRmlXIVajd5dhpDNsG7WJRCdlcS1wXY03OGH1rxm7p6i3QzJvTVyKEAcW0T9KojvLKakV2uTICceELqSMw==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@openpgp/web-stream-tools/-/web-stream-tools-0.0.13.tgz", + "integrity": "sha512-VQ0O0lUcD9ilLcMLQMJMgPhp8fDgMd4copd+UhSBGjud0vbI1ONQ3ffAhixEMml/AApLJtqCpd7PJcccPliFSA==", "dev": true, - "dependencies": { - "@mattiasbuelens/web-streams-adapter": "~0.1.0", - "web-streams-polyfill": "~3.0.3" + "peerDependencies": { + "typescript": ">=4.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@pnpm/network.ca-file": { diff --git a/package.json b/package.json index b1d02131e9a..95d47817377 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "graceful-fs": "4.1.13" }, "devDependencies": { - "@openpgp/web-stream-tools": "^0.0.11", + "@openpgp/web-stream-tools": "^0.0.13", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", "@types/chrome": "0.0.212", From d1b6613707f2d359e47ddcb8f4ed03276324bac0 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Wed, 8 Feb 2023 09:28:11 -0400 Subject: [PATCH 2/7] #4282 fix: duplicated contact search result (#4944) * fix: dupliated contact search result * feat: added ui test * fix: pr review --- .../js/common/platform/store/contact-store.ts | 3 ++- test/source/browser/controllable.ts | 15 ++++++++++++--- test/source/tests/compose.ts | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index eb65ed664cd..a66dedfd650 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -967,7 +967,8 @@ export class ContactStore extends AbstractStore { reject ); }); - return raw; + // Remove duplicated results + return raw.filter((value, index, arr) => arr.findIndex(contact => contact.email === value.email) === index); }; private static normalizeString = (str: string) => { diff --git a/test/source/browser/controllable.ts b/test/source/browser/controllable.ts index adfa3e23c1b..6efa74f7722 100644 --- a/test/source/browser/controllable.ts +++ b/test/source/browser/controllable.ts @@ -461,9 +461,9 @@ abstract class ControllableBase { throw new Error(`Could not find any frame in ${appearIn}s that matches ${urlMatchables.join(' ')}`); }; - public ensureElementsCount = async (selector: string, count: number) => { - const elements = await this.target.$$(selector); - expect(elements.length).to.equal(count); + public ensureElementsCount = async (selector: string, expectedCount: number) => { + const actualCount = await this.elementCount(selector); + expect(actualCount).to.equal(expectedCount); }; public getFrame = async (urlMatchables: string[], { sleep = 1, timeout = 10 } = { sleep: 1, timeout: 10 }): Promise => { @@ -560,6 +560,15 @@ abstract class ControllableBase { } }; + protected elementCount = async (selector: string): Promise => { + selector = this.selector(selector); + if (this.isXpath(selector)) { + return (await this.target.$x(selector)).length; + } else { + return (await this.target.$$(selector)).length; + } + }; + protected selsAsProcessedArr = (selector: string | string[]): string[] => { return (Array.isArray(selector) ? selector : [selector]).map(this.selector); }; diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 75d446d1b44..b383c974dd6 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -190,11 +190,13 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage1.type('@input-to', 'human'); // test guessing of contacts await composePage1.waitAll(['@container-contacts', '@action-select-contact-name(Human at FlowCrypt)']); await composePage1.waitAll(['@container-contacts', '@action-select-contact-email(human@flowcrypt.com)']); + await composePage1.ensureElementsCount('@action-select-contact-email(human@flowcrypt.com)', 1); // works on subsequent search const composePage2 = await ComposePageRecipe.openStandalone(t, browser, 'compose'); await composePage2.type('@input-to', 'human'); // test guessing of contacts await composePage2.waitAll(['@container-contacts', '@action-select-contact-name(Human at FlowCrypt)']); await composePage2.waitAll(['@container-contacts', '@action-select-contact-email(human@flowcrypt.com)']); + await composePage1.ensureElementsCount('@action-select-contact-email(human@flowcrypt.com)', 1); }) ); From a4f14d68a07308f8ff0e78435b450b8c7f04a5a8 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 9 Feb 2023 15:28:53 +0200 Subject: [PATCH 3/7] #4940 Update build script to not produce .bak files (#4945) * update build script * rename STREAMS_OUTDIR to STREAMS_FILES --- scripts/build.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 8ad925184f5..f8853291e39 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -70,7 +70,13 @@ mkdir -p $OUTDIR/lib/streams cp node_modules/@openpgp/web-stream-tools/lib/*.js $OUTDIR/lib/streams # patch imports with .js, e.g. replace './streams' with './streams.js' # until https://github.com/openpgpjs/web-stream-tools/pull/20 is resolved -sed -i.bak -E "s/'\.\/(streams|util|writer|reader|node-conversions)'/'\.\/\1\.js'/g" $OUTDIR/lib/streams/* +STREAMS_REGEX="s/'\.\/(streams|util|writer|reader|node-conversions)'/'\.\/\1\.js'/g" +STREAMS_FILES=$OUTDIR/lib/streams/* +if [[ "$OSTYPE" =~ ^darwin ]]; then # macOS needs additional parameter for backup files + sed -i '' -E $STREAMS_REGEX $STREAMS_FILES +else + sed -i -E $STREAMS_REGEX $STREAMS_FILES +fi # to update node-forge library, which is missing the non-minified version in dist, we have to build it manually # cd ~/git && rm -rf ./forge && git clone https://github.com/digitalbazaar/forge.git && cd ./forge && npm install && npm run-script build From 611dd461cbc5c88bed431cf45930f09cc74aec81 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 13 Feb 2023 07:10:08 -0400 Subject: [PATCH 4/7] #4844 feat: renew id token when expires (#4949) * feat: renew id token when expires * fix: pr reviews --- .../api/account-servers/external-service.ts | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/extension/js/common/api/account-servers/external-service.ts b/extension/js/common/api/account-servers/external-service.ts index 66bd8ce5116..392f0308cf6 100644 --- a/extension/js/common/api/account-servers/external-service.ts +++ b/extension/js/common/api/account-servers/external-service.ts @@ -12,6 +12,7 @@ import { ParsedRecipients } from '../email-provider/email-provider-api.js'; import { Buf } from '../../core/buf.js'; import { ClientConfigurationError, ClientConfigurationJson } from '../../client-configuration.js'; import { InMemoryStore } from '../../platform/store/in-memory-store.js'; +import { GoogleAuth } from '../email-provider/gmail/google-auth.js'; // todo - decide which tags to use type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'import-prv'; @@ -171,18 +172,43 @@ export class ExternalService extends Api { } else if (method !== 'GET') { reqFmt = 'JSON'; } - return await ExternalService.apiCall( - this.url, - path, - vals, - reqFmt, - progress, - { - ...headers, - ...(await this.authHdr()), - }, - 'json', - method - ); + try { + return await ExternalService.apiCall( + this.url, + path, + vals, + reqFmt, + progress, + { + ...headers, + ...(await this.authHdr()), + }, + 'json', + method + ); + } catch (firstAttemptErr) { + const idToken = await InMemoryStore.get(this.acctEmail, InMemoryStoreKeys.ID_TOKEN); + if (ApiErr.isAuthErr(firstAttemptErr) && idToken) { + // force refresh token + const { email } = GoogleAuth.parseIdToken(idToken); + if (email) { + return await ExternalService.apiCall( + this.url, + path, + vals, + reqFmt, + progress, + { + ...headers, + // eslint-disable-next-line @typescript-eslint/naming-convention + Authorization: await GoogleAuth.googleApiAuthHeader(email, true), + }, + 'json', + method + ); + } + } + throw firstAttemptErr; + } }; } From da4363a6eb51121ae585fc36f2273ce209aae04e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 13 Feb 2023 18:25:29 +0300 Subject: [PATCH 5/7] Employ user verification mechanisms from OpenPGP v5 (#4946) * reuse getPrimeryUser method * verifying key users * allow localhost domain for email address * fix * fix and test * use verified users in key-import-ui * PR review fixes --------- Co-authored-by: Roman Shevchenko --- extension/js/common/core/common.ts | 5 +- .../js/common/core/crypto/pgp/openpgp-key.ts | 81 ++++++++----------- extension/js/common/ui/key-import-ui.ts | 16 ++-- test/source/tests/unit-node.ts | 36 +++++++++ 4 files changed, 80 insertions(+), 58 deletions(-) diff --git a/extension/js/common/core/common.ts b/extension/js/common/core/common.ts index ef98bcc5aa2..7724e604e45 100644 --- a/extension/js/common/core/common.ts +++ b/extension/js/common/core/common.ts @@ -87,8 +87,9 @@ export class Str { if (email.indexOf(' ') !== -1) { return false; } - email = email.replace(/\:8001$/, ''); // for MOCK tests until https://github.com/FlowCrypt/flowcrypt-browser/issues/4631 - return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i.test( + email = email.replace(/\:8001$/, ''); // for MOCK tests, todo: remove from production + // `localhost` is a valid top-level domain for an email address, otherwise we require a second-level domain to be present + return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|localhost|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i.test( email ); }; diff --git a/extension/js/common/core/crypto/pgp/openpgp-key.ts b/extension/js/common/core/crypto/pgp/openpgp-key.ts index b4c2b1560dc..a95ae1b5702 100644 --- a/extension/js/common/core/crypto/pgp/openpgp-key.ts +++ b/extension/js/common/core/crypto/pgp/openpgp-key.ts @@ -305,9 +305,10 @@ export class OpenPGPKey { const key = await OpenPGPKey.extractExternalLibraryObjFromKey(pubkey); const result = new Map(); result.set(`Is Private?`, KeyUtil.formatResult(key.isPrivate())); - for (let i = 0; i < key.users.length; i++) { + const users = await OpenPGPKey.verifyAllUsers(key); + for (let i = 0; i < users.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result.set(`User id ${i}`, key.users[i].userID!.userID); + result.set(`User id ${i}`, (users[i].valid ? '' : '* REVOKED, INVALID OR MISSING SIGNATURE * ') + users[i].userID); } const user = await key.getPrimaryUser(); result.set(`Primary User`, user?.user?.userID?.userID || 'No primary user'); @@ -470,17 +471,6 @@ export class OpenPGPKey { return keyPacket.isDecrypted() === true; }; - public static getPrimaryUserId = async (pubs: OpenPGP.Key[], keyid: OpenPGP.KeyID): Promise => { - for (const opgpkey of pubs) { - const matchingKeys = opgpkey.getKeys(keyid); - if (matchingKeys.length > 0) { - const primaryUser = await opgpkey.getPrimaryUser(); - return primaryUser?.user?.userID?.userID; - } - } - return undefined; - }; - public static verify = async (msg: OpenpgpMsgOrCleartext, pubs: PubkeyInfo[]): Promise => { // todo: double-check if S/MIME ever gets here const validKeys = pubs.filter(x => !x.revoked && x.pubkey.family === 'openpgp').map(x => x.pubkey); @@ -546,39 +536,35 @@ export class OpenPGPKey { private static getExpirationAsDateOrUndefined = (expirationTime: Date | typeof Infinity | null) => { return expirationTime instanceof Date ? expirationTime : undefined; // we don't differ between Infinity and null }; - private static getUsersAndSelfCertifications = async (key: OpenPGP.Key) => { - const data = ( - await Promise.all( - key.users - .filter(user => user?.userID) - .map(async user => { - const dataToVerify = { userID: user.userID, key: key.keyPacket }; - const selfCertification = await OpenPGPKey.getLatestValidSignature( - user.selfCertifications, - key.keyPacket, - opgp.enums.signature.certGeneric, - dataToVerify - ); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return { userid: user.userID!.userID, email: user.userID!.email, selfCertification }; - }) - ) - ).filter(x => x.selfCertification); - // sort the same way as OpenPGP.js does - data.sort((a, b) => { - const A = a.selfCertification!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - const B = b.selfCertification!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - return Number(A.revoked) - Number(B.revoked) || Number(B.isPrimaryUserID) - Number(A.isPrimaryUserID) || Number(B.created) - Number(A.created); - }); - return data; - }; - private static getSortedUserids = async (key: OpenPGP.Key): Promise<{ identities: string[]; emails: string[] }> => { - const data = await OpenPGPKey.getUsersAndSelfCertifications(key); - return { - identities: data.map(x => x.userid).filter(Boolean), - emails: data.map(x => x.email).filter(Boolean), // todo: toLowerCase()? - }; + // returns all the `key.users` preserving order with `valid` property + private static verifyAllUsers = async (key: OpenPGP.Key) => { + return await ( + key as unknown as { + // a type patch until https://github.com/openpgpjs/openpgpjs/pull/1594 is resolved + // eslint-disable-next-line no-null/no-null + verifyAllUsers(): Promise<{ userID: string; keyID: OpenPGP.KeyID; valid: boolean | null }[]>; + } + ).verifyAllUsers(); + }; + + private static getSortedUserids = async (key: OpenPGP.Key) => { + const primaryUser = await Catch.undefinedOnException(key.getPrimaryUser()); + // if there is no good enough user id to serve as primary identity, we assume other user ids are even worse + if (primaryUser?.user?.userID?.userID) { + const primaryUserId = primaryUser.user.userID.userID; + const identities = [ + primaryUserId, // put the "primary" identity first + // other identities go in indeterministic order + ...Value.arr.unique((await OpenPGPKey.verifyAllUsers(key)).filter(x => x.valid && x.userID !== primaryUserId).map(x => x.userID)), + ]; + const emails = identities.map(userid => Str.parseEmail(userid).email).filter(Boolean); + if (emails.length === identities.length) { + // OpenPGP.js uses RFC 5322 `email-addresses` parser, so we expect all identities to contain a valid e-mail address + return { emails, identities }; + } + } + return { emails: [], identities: [] }; }; // mimicks OpenPGP.helper.getLatestValidSignature @@ -669,7 +655,10 @@ export class OpenPGPKey { }; private static getPrimaryKeyFlags = async (key: OpenPGP.Key): Promise => { - const selfCertification = (await OpenPGPKey.getUsersAndSelfCertifications(key)).map(x => x.selfCertification).find(Boolean); + // Note: The selected selfCertification (and hence the flags) will differ based on the current date + const primaryUser = await Catch.undefinedOnException(key.getPrimaryUser()); + // a type patch until https://github.com/openpgpjs/openpgpjs/pull/1594 is resolved + const selfCertification = (primaryUser as { selfCertification: OpenPGP.SignaturePacket } | undefined)?.selfCertification; if (!selfCertification) { return 0; } diff --git a/extension/js/common/ui/key-import-ui.ts b/extension/js/common/ui/key-import-ui.ts index 9f8f4c1b39f..df473db28e3 100644 --- a/extension/js/common/ui/key-import-ui.ts +++ b/extension/js/common/ui/key-import-ui.ts @@ -112,19 +112,15 @@ export class KeyImportUi { Ui.event.handle(async target => { $('.action_add_private_key').addClass('btn_disabled').attr('disabled'); $('.input_email_alias').prop('checked', false); - const prv = await Catch.undefinedOnException(opgp.readKey({ armoredKey: String($(target).val()) })); + const prv = await Catch.undefinedOnException(KeyUtil.parse(String($(target).val()))); if (prv !== undefined) { $('.action_add_private_key').removeClass('btn_disabled').removeAttr('disabled'); if (submitKeyForAddrs !== undefined) { - const users = prv.users; - for (const user of users) { - const userId = user.userID; - if (userId) { - for (const inputCheckboxesWithEmail of $('.input_email_alias')) { - if (String($(inputCheckboxesWithEmail).data('email')) === userId.email) { - KeyImportUi.addAliasForSubmission(userId.email, submitKeyForAddrs); - $(inputCheckboxesWithEmail).prop('checked', true); - } + for (const email of prv.emails) { + for (const inputCheckboxesWithEmail of $('.input_email_alias')) { + if (String($(inputCheckboxesWithEmail).data('email')) === email) { + KeyImportUi.addAliasForSubmission(email, submitKeyForAddrs); + $(inputCheckboxesWithEmail).prop('checked', true); } } } diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index da4d1cb6be9..70c8abbd019 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -1391,6 +1391,42 @@ jSB6A93JmnQGIkAem/kzGkKclmfAdGfc4FS+3Cn+6Q==Xmrz t.pass(); }); + test('[KeyUtil.diagnose] correctly displays revoked userid', async t => { + const key = await KeyUtil.parse(`-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: FlowCrypt Testing Only unspecified + +lFgEX6UIExYJKwYBBAHaRw8BAQdAMfHf64wPQ2LC9In5AKYU/KT1qWvI7e7aXr+L +WeQGUKIAAQCcB3zZlHfepQT26LIwbTDn4lvQ9LuD1fk2hK6i9FXFxxO7tBI8dXNl +ckBleGFtcGxlLmNvbT6IjwQQFgoAIAUCX6UIEwYLCQcIAwIEFQgKAgQWAgEAAhkB +AhsDAh4BACEJEEoCtcZ3snFuFiEENY1GQZqrKQqgUAXASgK1xneycW6P6AEA5iXF +K+fWpj0vn3xpKEuFRqvytPKFzhwd4wEvL+IGSPEBALE/pZdMzsDoKPENiLFpboDV +NVJScwFXIleKmtNaRycFiIwEExYIAD4FAmLqt7IJEEoCtcZ3snFuFiEENY1GQZqr +KQqgUAXASgK1xneycW4CngECmwMEFgIBAAYLCQcIAwIEFQgKAgAA7VwA/3x+J0i5 +DPaKtiosXHEV3LnOjaDGJgQlj7bR1BD4P62RAP0To1EcOvYk3qdgwda00oDkvYon +aAtVAK9dqadkbOI4D4h7BDAWCAAtBQJi6reyCRBKArXGd7JxbhYhBDWNRkGaqykK +oFAFwEoCtcZ3snFuAocAAh0gAABfXQEAvxCRqQz9r7iyrPyo4R/xF1BajPxoHd0Q +y4GYx/aIq5UA/19k0C/X7tH+fPJEd3Z2QjlrvyTbymUa+z4YGK1rh/YHtA9maXJz +dEBtb2NrLnRlc3SIjwQTFggAQQUCYuq3sgkQSgK1xneycW4WIQQ1jUZBmqspCqBQ +BcBKArXGd7JxbgKeAQKbAwQWAgEABgsJBwgDAgQVCAoCApkBAACNnQEA8tTL+tGS +wC9u4ECmo2Y8AUa0nvvv9+JmiMQphqldxD0A/jkDmtuj+KX8zxArkwC4IKCAFd2G +cdgj1z2/dAKVWmICnF0EX6UIExIKKwYBBAGXVQEFAQEHQBDdeawWVNqYkP8c/ihL +EUlVpn8cQw7rmRc/sIhdAXhfAwEIBwAA/0Jy7IelcHDjxE3OzagEzSxNrCVw8uPH +NRl8s6iP+CQYEfGIeAQYFggACQUCX6UIEwIbDAAhCRBKArXGd7JxbhYhBDWNRkGa +qykKoFAFwEoCtcZ3snFuWp8BAIzRBYJSfZzlvlyyPhrbXJoYSICGNy/5x7noXjp/ +ByeOAQDnTbQi4XwXJrU4A8Nl9eyz16ZWUzEPwfWgahIG1eQDDA== +=eyAR +-----END PGP PRIVATE KEY BLOCK-----`); + expect(key.identities).to.have.length(1); + expect(key.identities).to.eql(['first@mock.test']); + expect(key.emails).to.have.length(1); + expect(key.emails).to.eql(['first@mock.test']); + const result = await KeyUtil.diagnose(key, ''); + expect(result.get('Primary User')).to.equal('first@mock.test'); + expect(result.get('User id 0')).to.equal('* REVOKED, INVALID OR MISSING SIGNATURE * '); + expect(result.get('User id 1')).to.equal('first@mock.test'); + t.pass(); + }); + test('[KeyUtil.diagnose] displays PK and SK usage', async t => { const usageRegex = /\[\-\] \[(.*)\]/; /* eslint-disable @typescript-eslint/no-non-null-assertion */ From 09b17a844393a1e4924b281d7a35dc440730b44b Mon Sep 17 00:00:00 2001 From: Mart G <46025304+martgil@users.noreply.github.com> Date: Mon, 20 Feb 2023 15:59:56 +0800 Subject: [PATCH 6/7] prevent pasting large input on secure compose (#4914) * prevent pasting large input on secure compose * Added type definition for SquireEditor.getRoot() * apply requested change * update * cleanup * update * consider selection * add warning modal * update * update * update --------- Co-authored-by: Roman Shevchenko Co-authored-by: Roman Shevchenko --- .../compose-modules/compose-input-module.ts | 17 +++++++++++++++- extension/chrome/elements/compose.ts | 20 +++++++++++++++++++ extension/js/common/lang.ts | 1 + extension/types/squire.d.ts | 1 + 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/extension/chrome/elements/compose-modules/compose-input-module.ts b/extension/chrome/elements/compose-modules/compose-input-module.ts index 0afaa631b05..762f9fbcfd8 100644 --- a/extension/chrome/elements/compose-modules/compose-input-module.ts +++ b/extension/chrome/elements/compose-modules/compose-input-module.ts @@ -10,7 +10,9 @@ import { ParsedRecipients } from '../../../js/common/api/email-provider/email-pr import { Str } from '../../../js/common/core/common.js'; import { Xss } from '../../../js/common/platform/xss.js'; import { ViewModule } from '../../../js/common/view-module.js'; +import { Ui } from '../../../js/common/browser/ui.js'; import { ComposeView } from '../compose.js'; +import { Lang } from '../../../js/common/lang.js'; export class ComposeInputModule extends ViewModule { public squire = new window.Squire(this.view.S.cached('input_text').get(0)); @@ -78,12 +80,25 @@ export class ComposeInputModule extends ViewModule { return this.view.sendBtnModule.popover.choices.richtext; }; + public willInputLimitBeExceeded = (textToPaste: string, targetInputField: HTMLElement, selectionLengthGetter: () => number | undefined) => { + const limit = 50000; + const toBeRemoved = selectionLengthGetter() || 0; + const currentLength = targetInputField.innerText.trim().length; + const isInputLimitExceeded = currentLength - toBeRemoved + textToPaste.length > limit; + return isInputLimitExceeded; + }; + private handlePaste = () => { - this.squire.addEventListener('willPaste', (e: WillPasteEvent) => { + this.squire.addEventListener('willPaste', async (e: WillPasteEvent) => { const div = document.createElement('div'); div.appendChild(e.fragment); const html = div.innerHTML; const sanitized = this.isRichText() ? Xss.htmlSanitizeKeepBasicTags(html, 'IMG-KEEP') : Xss.htmlSanitizeAndStripAllTags(html, '
', false); + if (this.willInputLimitBeExceeded(sanitized, this.squire.getRoot(), () => this.squire.getSelectedText().length)) { + e.preventDefault(); + await Ui.modal.warning(Lang.compose.inputLimitExceededOnPaste); + return; + } Xss.setElementContentDANGEROUSLY(div, sanitized); // xss-sanitized e.fragment.appendChild(div); }); diff --git a/extension/chrome/elements/compose.ts b/extension/chrome/elements/compose.ts index d7311b88c6a..a32d8564bf1 100644 --- a/extension/chrome/elements/compose.ts +++ b/extension/chrome/elements/compose.ts @@ -31,6 +31,7 @@ import { PubLookup } from '../../js/common/api/pub-lookup.js'; import { AcctStore } from '../../js/common/platform/store/acct-store.js'; import { AccountServer } from '../../js/common/api/account-server.js'; import { ComposeReplyBtnPopoverModule } from './compose-modules/compose-reply-btn-popover-module.js'; +import { Lang } from '../../js/common/lang.js'; export class ComposeView extends View { public readonly acctEmail: string; @@ -222,6 +223,25 @@ export class ComposeView extends View { 'click', this.setHandler(async () => await this.renderModule.openSettingsWithDialog('help'), this.errModule.handle(`help dialog`)) ); + this.S.cached('input_intro').on( + 'paste', + this.setHandler(async (el, ev) => { + const clipboardEvent = ev.originalEvent as ClipboardEvent; + if (clipboardEvent.clipboardData) { + const isInputLimitExceeded = this.inputModule.willInputLimitBeExceeded(clipboardEvent.clipboardData.getData('text/plain'), el, () => { + const selection = window.getSelection(); + if (selection && selection.anchorNode === selection.focusNode && selection.anchorNode?.parentElement === el) { + return Math.abs(selection.anchorOffset - selection.focusOffset); + } + return 0; + }); + if (isInputLimitExceeded) { + ev.preventDefault(); + await Ui.modal.warning(Lang.compose.inputLimitExceededOnPaste); + } + } + }) + ); this.attachmentsModule.setHandlers(); this.inputModule.setHandlers(); this.myPubkeyModule.setHandlers(); diff --git a/extension/js/common/lang.ts b/extension/js/common/lang.ts index e3d283c3d53..b19e32982fa 100644 --- a/extension/js/common/lang.ts +++ b/extension/js/common/lang.ts @@ -120,6 +120,7 @@ export const Lang = { enterprisePasswordPolicy: 'Please use password with the following properties:\n - one uppercase\n - one lowercase\n - one number\n - one special character eg &"#-\'_%-@,;:!*()\n - 8 characters length', consumerPasswordPolicy: 'Please use a password at least 8 characters long', + inputLimitExceededOnPaste: "The paste operation can't be completed because the resulting text size would exceed the allowed limit of 50K.", }, general: { contactMinimalSubsentence, diff --git a/extension/types/squire.d.ts b/extension/types/squire.d.ts index 9f4482466d7..26ddacfba5d 100644 --- a/extension/types/squire.d.ts +++ b/extension/types/squire.d.ts @@ -71,6 +71,7 @@ export declare class SquireEditor { removeAllFormatting(): void; changeFormat(formattingToAdd: any, formattingToRemove: any, range: Range): void; setConfig(config: any): SquireEditor; + getRoot(): HTMLElement; } export declare class WillPasteEvent extends ClipboardEvent { From 54b0e8019da2a4c8e4fdefd3d2ac9f83750229cc Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 22 Feb 2023 09:16:19 +0000 Subject: [PATCH 7/7] fix --- conf/tsconfig.content_scripts.json | 2 +- conf/tsconfig.test.json | 5 +---- tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/conf/tsconfig.content_scripts.json b/conf/tsconfig.content_scripts.json index 1fceafca26e..b5bcb994288 100644 --- a/conf/tsconfig.content_scripts.json +++ b/conf/tsconfig.content_scripts.json @@ -18,7 +18,7 @@ "paths": { "dompurify": ["types/purify.d.ts"], "openpgp": ["../node_modules/openpgp/openpgp.d.ts"], - "@openpgp/web-stream-tools": ["../node_modules/@openpgp/web-stream-tools/web-stream-tools.d.ts"] + "@openpgp/web-stream-tools": ["../node_modules/@openpgp/web-stream-tools/types/index.v4.9.d.ts"] }, "typeRoots": ["../extension/types", "../extension/js/common/core/types"] }, diff --git a/conf/tsconfig.test.json b/conf/tsconfig.test.json index 9c2f3b1aa11..0aa1ba1d290 100644 --- a/conf/tsconfig.test.json +++ b/conf/tsconfig.test.json @@ -12,10 +12,7 @@ "outDir": "../build/test", "skipLibCheck": true, "paths": { - "@openpgp/web-stream-tools": [ - "../node_modules/@openpgp/web-stream-tools/web-stream-tools.d.ts", - "../build/streams/streams.js" - ] + "@openpgp/web-stream-tools": ["../node_modules/@openpgp/web-stream-tools/types/index.v4.9.d.ts", "../build/streams/streams.js"] } }, "files": [ diff --git a/tsconfig.json b/tsconfig.json index 5805c360d77..23f761b6a12 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "jquery": ["lib/jquery.min.js", "COMMENT"], "sweetalert2": ["lib/sweetalert2.js", "COMMENT"], "openpgp": ["../node_modules/openpgp/openpgp.d.js", "lib/openpgp.js", "COMMENT"], - "@openpgp/web-stream-tools": ["../node_modules/@openpgp/web-stream-tools/web-stream-tools.d.ts", "lib/streams/streams.js"], + "@openpgp/web-stream-tools": ["../node_modules/@openpgp/web-stream-tools/types/index.v4.9.d.ts", "lib/streams/streams.js"], "dompurify": ["types/purify.d.ts", "lib/purify.js", "COMMENT"], "fine-uploader": ["lib/fine-uploader.js", "COMMENT"], "clipboard": ["lib/clipboard.js", "COMMENT"],