Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conf/tsconfig.content_scripts.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
},
Expand Down
5 changes: 1 addition & 4 deletions conf/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComposeView> {
public squire = new window.Squire(this.view.S.cached('input_text').get(0));
Expand Down Expand Up @@ -78,12 +80,25 @@ export class ComposeInputModule extends ViewModule<ComposeView> {
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, '<br>', 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);
});
Expand Down
20 changes: 20 additions & 0 deletions extension/chrome/elements/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
52 changes: 39 additions & 13 deletions extension/js/common/api/account-servers/external-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
};
}
5 changes: 3 additions & 2 deletions extension/js/common/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
};
Expand Down
81 changes: 35 additions & 46 deletions extension/js/common/core/crypto/pgp/openpgp-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,10 @@ export class OpenPGPKey {
const key = await OpenPGPKey.extractExternalLibraryObjFromKey(pubkey);
const result = new Map<string, string>();
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');
Expand Down Expand Up @@ -470,17 +471,6 @@ export class OpenPGPKey {
return keyPacket.isDecrypted() === true;
};

public static getPrimaryUserId = async (pubs: OpenPGP.Key[], keyid: OpenPGP.KeyID): Promise<string | undefined> => {
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<VerifyRes> => {
// todo: double-check if S/MIME ever gets here
const validKeys = pubs.filter(x => !x.revoked && x.pubkey.family === 'openpgp').map(x => x.pubkey);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -669,7 +655,10 @@ export class OpenPGPKey {
};

private static getPrimaryKeyFlags = async (key: OpenPGP.Key): Promise<OpenPGP.enums.keyFlags> => {
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;
}
Expand Down
1 change: 1 addition & 0 deletions extension/js/common/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion extension/js/common/platform/store/contact-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
16 changes: 6 additions & 10 deletions extension/js/common/ui/key-import-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions extension/types/squire.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 11 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading