Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
08cc689
wip: refactor
martgil Jul 24, 2024
dd6b230
wip: Register content script for Thunderbird
martgil Jul 24, 2024
87dea43
refactor: move Thunderbird event handler and content script registrat…
martgil Jul 24, 2024
951f944
wip: update manifest.json
martgil Jul 25, 2024
24e73d0
wip
martgil Jul 26, 2024
eb5a1b0
wip
martgil Jul 28, 2024
cd8dbb9
wip
martgil Jul 28, 2024
c4467b6
wip
martgil Jul 28, 2024
79c33b7
wip
martgil Jul 31, 2024
8ad4a5e
Merge remote-tracking branch 'origin/master' into issue-5667-added-me…
martgil Jul 31, 2024
183dfed
wip
martgil Jul 31, 2024
9e061d4
added functionality to obtain pgp message from a background script
martgil Jul 31, 2024
8cbe0ea
fix openpgp undefined in content-script
martgil Aug 1, 2024
e015143
wip
martgil Aug 1, 2024
3462ce6
Merge remote-tracking branch 'origin/master' into issue-5667-added-me…
martgil Aug 5, 2024
8b44889
Merge branch 'master' into issue-5667-added-message-decryption-on-thu…
martgil Aug 15, 2024
518d38b
Add "thunderbird_get_current_user" to BrowserMsg
martgil Aug 16, 2024
f3f99ab
Add "thunderbird_msg_decrypt" to BrowserMsg
martgil Aug 16, 2024
1472fd5
fix BrowserMsg listener names
martgil Aug 16, 2024
92b3263
Merge remote-tracking branch 'origin/master' into issue-5667-added-me…
martgil Aug 20, 2024
9afaf6e
wip
martgil Aug 21, 2024
abf898b
wip
martgil Aug 21, 2024
f901bc5
fix missing opgp reference in Thunderbird port
martgil Aug 21, 2024
ab34c37
Merge branch 'master' into issue-5667-added-message-decryption-on-thu…
martgil Aug 23, 2024
b719396
Added pgp message decryption
martgil Aug 24, 2024
fd54d62
fix failing test
martgil Aug 24, 2024
c19f7f4
run element replacer in heartbeat (runIntervalFunctionsPeriodically)
martgil Aug 24, 2024
8cf725f
Improved decrypted message rendering in Thunderbird
martgil Aug 24, 2024
25df0e6
Add cleartext signed message detection
martgil Aug 24, 2024
20a8b03
wip
martgil Aug 25, 2024
2cd3e47
Add signature verification in Thunderbird (cleartext message)
martgil Aug 25, 2024
7565afb
wip
martgil Aug 25, 2024
f89e496
wip: Better signature verification handling
martgil Aug 25, 2024
ba0ed46
wip
martgil Aug 25, 2024
cc7aba4
wip
martgil Aug 25, 2024
0dddb00
cleanup
martgil Aug 25, 2024
cf24357
Merge branch 'master' into issue-5667-added-message-decryption-on-thu…
martgil Aug 26, 2024
adafaa9
Add passphrase dialog prompt in Thunderbird
martgil Aug 27, 2024
8fba4f2
Add autoclose for completely unlock passphrase in Thunderbird
martgil Aug 27, 2024
3203a2f
add another case for encrypted message detection
martgil Aug 27, 2024
53589ef
fix unsafe assignment of an any value
martgil Aug 28, 2024
50ea4aa
better content script bundling
martgil Aug 28, 2024
d78693c
cleanup
martgil Aug 28, 2024
909684c
Add todo in preparation for post thunderbird development
martgil Aug 28, 2024
1f56012
inject scripts to thunderbird
sosnovsky Sep 4, 2024
3b6f330
Merge branch 'master' into 5667-thunderbird-fix
sosnovsky Sep 4, 2024
5088695
exclude sweetalert2 from thunderbird build
sosnovsky Sep 6, 2024
58ed00b
wip
sosnovsky Sep 6, 2024
987eb10
wip
sosnovsky Sep 6, 2024
dc212e0
Merge branch 'master' into 5667-thunderbird-fix
martgil Sep 11, 2024
cc34e7f
code improvements
sosnovsky Sep 11, 2024
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
14 changes: 14 additions & 0 deletions extension/chrome/elements/passphrase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ View.run(
BrowserMsg.send.passphraseEntry({ entered, initiatorFrameId });
};

private closeDialogPageOpenedExternally = async () => {
if (Catch.isThunderbirdMail() && window.top === window.self) {
const currentTab = await messenger.tabs.query({ active: true, currentWindow: true });
if (currentTab.length > 0) {
const tabId = currentTab[0].id;
if (tabId) {
await messenger.tabs.remove(tabId);
}
}
}
};

private submitHandler = async () => {
if (await this.bruteForceProtection.shouldDisablePassphraseCheck()) {
return;
Expand Down Expand Up @@ -237,10 +249,12 @@ View.run(
}
if (unlockCount && allPrivateKeys.length > 1) {
Ui.toast(`${unlockCount} of ${allPrivateKeys.length} keys ${unlockCount > 1 ? 'were' : 'was'} unlocked by this pass phrase`);
await this.closeDialogPageOpenedExternally();
}
if (atLeastOneMatched) {
await this.bruteForceProtection.passphraseCheckSucceed();
this.closeDialog(true, this.initiatorFrameId);
await this.closeDialogPageOpenedExternally();
} else {
await this.bruteForceProtection.passphraseCheckFailed();
this.renderFailedEntryPpPrompt();
Expand Down
2 changes: 1 addition & 1 deletion extension/css/cryptup.css
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ td {
.backup_neutral {
border: none;
border-left: 4px solid #989898;
padding-left: 10px;
padding: 6px 10px;
}

.pgp_neutral .error_container {
Expand Down
13 changes: 12 additions & 1 deletion extension/js/common/browser/browser-msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export namespace Bm {
export type ReRenderRecipient = { email: string };
export type PgpBlockRetry = { frameId: string; messageSender: Dest };
export type PgpBlockReady = { frameId: string; messageSender: Dest };
export type ThunderbirdOpenPassphraseDialog = { acctEmail: string; longids: string };

export namespace Res {
export type GetActiveTabInfo = {
Expand All @@ -112,6 +113,9 @@ export namespace Bm {
export type ExpirationCacheGet<V> = Promise<V | undefined>;
export type ExpirationCacheSet = Promise<void>;
export type ExpirationCacheDeleteExpired = Promise<void>;
export type ThunderbirdGetCurrentUser = string | undefined;
export type ThunderbirdMsgGet = messenger.messages.MessagePart | undefined;
export type ThunderbirdOpenPassphraseDialog = Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Db = any; // not included in Any below
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -128,7 +132,8 @@ export namespace Bm {
| ExpirationCacheSet
| ExpirationCacheDeleteExpired
| AjaxGmailAttachmentGetChunk
| ConfirmationResult;
| ConfirmationResult
| ThunderbirdMsgGet;
}

export type AnyRequest =
Expand Down Expand Up @@ -169,6 +174,7 @@ export namespace Bm {
| PgpBlockReady
| PgpBlockRetry
| ConfirmationResult
| ThunderbirdOpenPassphraseDialog
| Ajax;

export type AsyncRespondingHandler = (req: AnyRequest) => Promise<Res.Any>;
Expand Down Expand Up @@ -232,6 +238,11 @@ export class BrowserMsg {
BrowserMsg.sendAwait(undefined, 'expirationCacheSet', bm, true) as Promise<Bm.Res.ExpirationCacheSet>,
expirationCacheDeleteExpired: (bm: Bm.ExpirationCacheDeleteExpired) =>
BrowserMsg.sendAwait(undefined, 'expirationCacheDeleteExpired', bm, true) as Promise<Bm.Res.ExpirationCacheDeleteExpired>,
thunderbirdGetCurrentUser: () =>
BrowserMsg.sendAwait(undefined, 'thunderbirdGetCurrentUser', undefined, true) as Promise<Bm.Res.ThunderbirdGetCurrentUser>,
thunderbirdMsgGet: () => BrowserMsg.sendAwait(undefined, 'thunderbirdMsgGet', undefined, true) as Promise<Bm.Res.ThunderbirdMsgGet>,
thunderbirdOpenPassphraseDiaglog: (bm: Bm.ThunderbirdOpenPassphraseDialog) =>
BrowserMsg.sendAwait(undefined, 'thunderbirdOpenPassphraseDialog', bm, true) as Promise<Bm.Res.ThunderbirdOpenPassphraseDialog>,
},
},
passphraseEntry: (bm: Bm.PassphraseEntry) => {
Expand Down
2 changes: 1 addition & 1 deletion extension/js/common/browser/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class Env {
}

public static async webmails(): Promise<WebMailName[]> {
return ['gmail']; // async because storage may be involved in the future
return ['gmail', 'thunderbird']; // async because storage may be involved in the future
}

public static getBaseUrl() {
Expand Down
8 changes: 5 additions & 3 deletions extension/js/common/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ export class Injector {
}

public meta = () => {
this.S.cached('body')
.addClass(`cryptup_${this.webmailName} cryptup_${this.webmailName}_${this.webmailVariant} ${Catch.browser().name}`)
.append(this.factory.metaStylesheet('webmail') + this.factory.metaNotificationContainer()); // xss-safe-factory
if (this.webmailName === 'gmail') {
this.S.cached('body')
.addClass(`cryptup_${this.webmailName} cryptup_${this.webmailName}_${this.webmailVariant} ${Catch.browser().name}`)
.append(this.factory.metaStylesheet('webmail') + this.factory.metaNotificationContainer()); // xss-safe-factory
}
};

public openComposeWin = (draftId?: string, openInFullScreen?: boolean, thunderbirdMsgId?: number, replyOption?: string, replyMsgId?: string): boolean => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type WebmailVariantObject = {
type WebmailSpecificInfo = {
name: WebMailName;
variant: WebmailVariantString;
getUserAccountEmail: () => string | undefined;
getUserAccountEmail: () => Promise<string> | string | undefined;
getUserFullName: () => string | undefined;
getReplacer: () => WebmailElementReplacer;
start: (
Expand Down Expand Up @@ -74,7 +74,7 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi
let acctEmailInterval = 1000;
const webmails = await Env.webmails();
while (true) {
const acctEmail = webmailSpecific.getUserAccountEmail();
const acctEmail = await webmailSpecific.getUserAccountEmail();
if (typeof acctEmail !== 'undefined') {
win.account_email_global = acctEmail;
if (webmails.includes(webmailSpecific.name)) {
Expand Down Expand Up @@ -444,20 +444,22 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi
}
const acctEmail = await waitForAcctEmail();
const { tabId, notifications, factory, inject } = await initInternalVars(acctEmail);
await showNotificationsAndWaitTilAcctSetUp(acctEmail, notifications);
Catch.setHandledTimeout(() => updateClientConfiguration(acctEmail), 0);
const ppEvent: { entered?: boolean } = {};
const relayManager = new RelayManager();
browserMsgListen(acctEmail, tabId, inject, factory, notifications, relayManager, ppEvent);
const clientConfiguration = await ClientConfiguration.newInstance(acctEmail);
await startPullingKeysFromEkm(
acctEmail,
clientConfiguration,
factory,
ppEvent,
notifications,
Catch.try(() => notifyExpiringKeys(acctEmail, clientConfiguration, notifications))
);
if (webmailSpecific.name === 'gmail') {
Catch.setHandledTimeout(() => updateClientConfiguration(acctEmail), 0);
await showNotificationsAndWaitTilAcctSetUp(acctEmail, notifications);
browserMsgListen(acctEmail, tabId, inject, factory, notifications, relayManager, ppEvent);
await startPullingKeysFromEkm(
acctEmail,
clientConfiguration,
factory,
ppEvent,
notifications,
Catch.try(() => notifyExpiringKeys(acctEmail, clientConfiguration, notifications))
);
}
await webmailSpecific.start(acctEmail, clientConfiguration, inject, notifications, factory, relayManager);
} catch (e) {
if (e instanceof TabIdRequiredError) {
Expand Down Expand Up @@ -509,7 +511,11 @@ export const contentScriptSetupIfVacant = async (webmailSpecific: WebmailSpecifi
};

win.vacant = () => {
return !$('.' + win.destroyable_class).length;
if (Catch.isThunderbirdMail()) {
return true;
} else {
return !$('.' + win.destroyable_class).length;
}
};

win.TrySetDestroyableInterval = (code, ms) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,144 @@

'use strict';

import { IntervalFunction, WebmailElementReplacer } from '../generic/webmail-element-replacer';
import { BrowserMsg } from '../../../common/browser/browser-msg.js';
import { KeyUtil } from '../../../common/core/crypto/key.js';
import { DecryptError, DecryptErrTypes, MsgUtil } from '../../../common/core/crypto/pgp/msg-util.js';
import { OpenPGPKey } from '../../../common/core/crypto/pgp/openpgp-key.js';
import { PgpArmor } from '../../../common/core/crypto/pgp/pgp-armor';
import { Catch } from '../../../common/platform/catch';
import { ContactStore } from '../../../common/platform/store/contact-store.js';
import { KeyStore } from '../../../common/platform/store/key-store.js';
import { Xss } from '../../../common/platform/xss.js';
import { IntervalFunction, WebmailElementReplacer } from '../generic/webmail-element-replacer.js';
import * as openpgp from 'openpgp';

export class ThunderbirdElementReplacer extends WebmailElementReplacer {
public getIntervalFunctions: () => IntervalFunction[];
public setReplyBoxEditable: () => Promise<void>;
public reinsertReplyBox: (replyMsgId: string) => void;
public scrollToReplyBox: (replyMsgId: string) => void;
public scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void;
private emailBodyFromThunderbirdMail: string;

public getIntervalFunctions = (): IntervalFunction[] => {
return [{ interval: 2000, handler: () => this.replaceThunderbirdMsgPane() }];
};

public replaceThunderbirdMsgPane = async () => {
if (Catch.isThunderbirdMail()) {
const fullMsg = await BrowserMsg.send.bg.await.thunderbirdMsgGet();
if (!fullMsg) {
return;
} else {
const acctEmail = await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser();
const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, String(acctEmail)))?.sortedPubkeys ?? [];
const signerKeys = parsedPubs.map(key => KeyUtil.armor(key.pubkey));
if (this.isPublicKeyEncryptedMsg(fullMsg)) {
const result = await MsgUtil.decryptMessage({
kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(String(acctEmail)),
encryptedData: this.emailBodyFromThunderbirdMail,
verificationPubs: signerKeys,
});
if (result.success && result.content) {
const decryptedMsg = result.content.toUtfStr();
const encryptionStatus = result.isEncrypted ? 'encrypted' : 'not encrypted';
let verificationStatus = '';
if (result?.signature) {
if (result.signature.match) {
verificationStatus = 'signed';
} else if (result.signature.error) {
verificationStatus = `could not verify signature: ${result.signature.error}`;
} else {
verificationStatus = 'not signed';
}
}
const pgpBlock = this.generatePgpBlockTemplate(encryptionStatus, verificationStatus, decryptedMsg);
$('body').html(pgpBlock); // xss-sanitized
} else {
const decryptErr = result as DecryptError;
let decryptionErrorMsg = '';
if (decryptErr.error && decryptErr.error.type === DecryptErrTypes.needPassphrase) {
const acctEmail = String(await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser());
const longids = decryptErr.longids.needPassphrase.join(',');
decryptionErrorMsg = `decrypt error: private key needs to be unlocked by your passphrase.`;
await BrowserMsg.send.bg.await.thunderbirdOpenPassphraseDiaglog({ acctEmail, longids });
} else {
decryptionErrorMsg = `decrypt error: ${(result as DecryptError).error.message}`;
}
const pgpBlock = this.generatePgpBlockTemplate(decryptionErrorMsg, 'not signed', this.emailBodyFromThunderbirdMail);
$('body').html(pgpBlock); // xss-sanitized
}
} else if (this.isCleartextMsg(fullMsg)) {
const message = await openpgp.readCleartextMessage({ cleartextMessage: this.emailBodyFromThunderbirdMail });
const result = await OpenPGPKey.verify(message, await ContactStore.getPubkeyInfos(undefined, signerKeys));
let verificationStatus = '';
let signedMessage = '';
if (result.match && result.content) {
verificationStatus = 'signed';
signedMessage = result.content.toUtfStr();
} else if (result.error) {
verificationStatus = `could not verify signature: ${result.error}`;
}
const pgpBlock = this.generatePgpBlockTemplate('not encrypted', verificationStatus, signedMessage);
$('body').html(pgpBlock); // xss-sanitized
}
// todo: detached signed message via https://github.com/FlowCrypt/flowcrypt-browser/issues/5668
}
}
};

private generatePgpBlockTemplate = (encryptionStatus: string, verificationStatus: string, messageToRender: string): string => {
return `
<div ${encryptionStatus === 'encrypted' ? 'class="pgp_secure"' : 'class="pgp_neutral"'}>
<div>
<div id="pgp_encryption" class="pgp_badge short ${encryptionStatus === 'encrypted' ? 'green_label' : 'red_label'}">${encryptionStatus}</div>
<div id="pgp_signature" class="pgp_badge short ${verificationStatus === 'signed' ? 'green_label' : 'red_label'}">${verificationStatus}</div>
</div>
<div class="pgp_block">
<pre>${Xss.escape(messageToRender)}</pre>
</div>
</div>`;
};

private isCleartextMsg = (fullMsg: messenger.messages.MessagePart): boolean => {
return (
(fullMsg.headers &&
'openpgp' in fullMsg.headers &&
fullMsg.parts &&
fullMsg.parts[0]?.parts?.length === 1 &&
fullMsg.parts[0].parts[0].contentType === 'text/plain' &&
this.resemblesCleartextMsg(fullMsg.parts[0].parts[0].body?.trim() || '')) ||
false
);
};

private resemblesCleartextMsg = (body: string) => {
this.emailBodyFromThunderbirdMail = body;
return (
body.startsWith(PgpArmor.ARMOR_HEADER_DICT.signedMsg.begin) &&
body.includes(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.middle)) &&
body.endsWith(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.end))
);
};

private isPublicKeyEncryptedMsg = (fullMsg: messenger.messages.MessagePart): boolean => {
if (fullMsg.headers && 'openpgp' in fullMsg.headers && fullMsg.parts) {
return (
(fullMsg.parts[0]?.parts?.length === 2 &&
fullMsg.parts[0]?.parts[1].contentType === 'application/pgp-encrypted' &&
this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) ||
(fullMsg.parts[0]?.parts?.length === 1 &&
fullMsg.parts[0]?.contentType === 'multipart/mixed' &&
this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) ||
(fullMsg.parts.length === 1 && fullMsg.parts[0]?.contentType === 'text/plain' && this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.body?.trim() || '')) ||
false
);
}
return false;
};

private resemblesAsciiArmoredMsg = (body: string): boolean => {
this.emailBodyFromThunderbirdMail = body;
return body.startsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.begin) && body.endsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.end as string);
};
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact [email protected] */
import { ClientConfiguration } from '../../../common/client-configuration';
import { Injector } from '../../../common/inject';
import { Notifications } from '../../../common/notifications';
import { BrowserMsg } from '../../../common/browser/browser-msg';
import { contentScriptSetupIfVacant } from '../generic/setup-webmail-content-script';
import { GmailElementReplacer } from '../gmail/gmail-element-replacer';
import { ThunderbirdElementReplacer } from './thunderbird-element-replacer';

export class ThunderbirdWebmailStartup {
private replacer: GmailElementReplacer;
private replacer: ThunderbirdElementReplacer;

public asyncConstructor = async () => {
await contentScriptSetupIfVacant({
name: 'thunderbird',
variant: undefined,
getUserAccountEmail: () => undefined, // todo, but can start with undefined
getUserAccountEmail: async () => String(await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser()),
getUserFullName: () => undefined, // todo, but can start with undefined
getReplacer: () => new ThunderbirdElementReplacer(), // todo - add this class empty, methods do nothing
getReplacer: () => this.replacer,
start: this.start,
});
};

private start = async (
acctEmail: string,
clientConfiguration: ClientConfiguration,
injector: Injector,
notifications: Notifications
// factory: XssSafeFactory, // todo in another issue
// relayManager: RelayManager // todo in another issue
) => {
// injector.btns(); // todo in another issue - add compose button
private start = async () => {
this.replacer = new ThunderbirdElementReplacer();
this.replacer.runIntervalFunctionsPeriodically();
await notifications.showInitial(acctEmail);
notifications.show(
'FlowCrypt Thunderbird support is still in early development, and not expected to function properly yet. Support will be gradually added in upcoming versions.'
);
// todo: show notification using Thunderbird Notification as contentscript notification or such does not work.
// await notifications.showInitial(acctEmail);
// notifications.show(
// 'FlowCrypt Thunderbird support is still in early development, and not expected to function properly yet. Support will be gradually added in upcoming versions.'
// );
};
}
3 changes: 1 addition & 2 deletions extension/js/content_scripts/webmail/webmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ declare global {

Catch.try(async () => {
// when we support more webmails, there will be if/else here to figure out which one to run
const browserName = Catch.browser().name;
if (browserName === 'thunderbird') {
if (Catch.isThunderbirdMail()) {
await new ThunderbirdWebmailStartup().asyncConstructor();
} else {
await new GmailWebmailStartup().asyncConstructor();
Expand Down
7 changes: 5 additions & 2 deletions extension/js/service_worker/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ console.info('background.js service worker starting');
await BgHandlers.updateUninstallUrl({});
injectFcIntoWebmail();

// Thunderbird event handlers
if (Catch.isThunderbirdMail()) {
BrowserMsg.thunderbirdSecureComposeHandler();
BgHandlers.thunderbirdSecureComposeHandler();
await BgHandlers.thunderbirdContentScriptRegistration();
BrowserMsg.bgAddListener('thunderbirdGetCurrentUser', BgHandlers.thunderbirdGetCurrentUserHandler);
BrowserMsg.bgAddListener('thunderbirdMsgGet', BgHandlers.thunderbirdMsgGetHandler);
BrowserMsg.bgAddListener('thunderbirdOpenPassphraseDialog', BgHandlers.thunderbirdOpenPassphraseDialog);
}
})().catch(Catch.reportErr);
Loading
Loading