|
2 | 2 |
|
3 | 3 | 'use strict'; |
4 | 4 |
|
5 | | -import { IntervalFunction, WebmailElementReplacer } from '../generic/webmail-element-replacer'; |
| 5 | +import { BrowserMsg } from '../../../common/browser/browser-msg.js'; |
| 6 | +import { KeyUtil } from '../../../common/core/crypto/key.js'; |
| 7 | +import { DecryptError, DecryptErrTypes, MsgUtil } from '../../../common/core/crypto/pgp/msg-util.js'; |
| 8 | +import { OpenPGPKey } from '../../../common/core/crypto/pgp/openpgp-key.js'; |
| 9 | +import { PgpArmor } from '../../../common/core/crypto/pgp/pgp-armor'; |
| 10 | +import { Catch } from '../../../common/platform/catch'; |
| 11 | +import { ContactStore } from '../../../common/platform/store/contact-store.js'; |
| 12 | +import { KeyStore } from '../../../common/platform/store/key-store.js'; |
| 13 | +import { Xss } from '../../../common/platform/xss.js'; |
| 14 | +import { IntervalFunction, WebmailElementReplacer } from '../generic/webmail-element-replacer.js'; |
| 15 | +import * as openpgp from 'openpgp'; |
6 | 16 |
|
7 | 17 | export class ThunderbirdElementReplacer extends WebmailElementReplacer { |
8 | | - public getIntervalFunctions: () => IntervalFunction[]; |
9 | 18 | public setReplyBoxEditable: () => Promise<void>; |
10 | 19 | public reinsertReplyBox: (replyMsgId: string) => void; |
11 | 20 | public scrollToReplyBox: (replyMsgId: string) => void; |
12 | 21 | public scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void; |
| 22 | + private emailBodyFromThunderbirdMail: string; |
| 23 | + |
| 24 | + public getIntervalFunctions = (): IntervalFunction[] => { |
| 25 | + return [{ interval: 2000, handler: () => this.replaceThunderbirdMsgPane() }]; |
| 26 | + }; |
| 27 | + |
| 28 | + public replaceThunderbirdMsgPane = async () => { |
| 29 | + if (Catch.isThunderbirdMail()) { |
| 30 | + const fullMsg = await BrowserMsg.send.bg.await.thunderbirdMsgGet(); |
| 31 | + if (!fullMsg) { |
| 32 | + return; |
| 33 | + } else { |
| 34 | + const acctEmail = await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser(); |
| 35 | + const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, String(acctEmail)))?.sortedPubkeys ?? []; |
| 36 | + const signerKeys = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); |
| 37 | + if (this.isPublicKeyEncryptedMsg(fullMsg)) { |
| 38 | + const result = await MsgUtil.decryptMessage({ |
| 39 | + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(String(acctEmail)), |
| 40 | + encryptedData: this.emailBodyFromThunderbirdMail, |
| 41 | + verificationPubs: signerKeys, |
| 42 | + }); |
| 43 | + if (result.success && result.content) { |
| 44 | + const decryptedMsg = result.content.toUtfStr(); |
| 45 | + const encryptionStatus = result.isEncrypted ? 'encrypted' : 'not encrypted'; |
| 46 | + let verificationStatus = ''; |
| 47 | + if (result?.signature) { |
| 48 | + if (result.signature.match) { |
| 49 | + verificationStatus = 'signed'; |
| 50 | + } else if (result.signature.error) { |
| 51 | + verificationStatus = `could not verify signature: ${result.signature.error}`; |
| 52 | + } else { |
| 53 | + verificationStatus = 'not signed'; |
| 54 | + } |
| 55 | + } |
| 56 | + const pgpBlock = this.generatePgpBlockTemplate(encryptionStatus, verificationStatus, decryptedMsg); |
| 57 | + $('body').html(pgpBlock); // xss-sanitized |
| 58 | + } else { |
| 59 | + const decryptErr = result as DecryptError; |
| 60 | + let decryptionErrorMsg = ''; |
| 61 | + if (decryptErr.error && decryptErr.error.type === DecryptErrTypes.needPassphrase) { |
| 62 | + const acctEmail = String(await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser()); |
| 63 | + const longids = decryptErr.longids.needPassphrase.join(','); |
| 64 | + decryptionErrorMsg = `decrypt error: private key needs to be unlocked by your passphrase.`; |
| 65 | + await BrowserMsg.send.bg.await.thunderbirdOpenPassphraseDiaglog({ acctEmail, longids }); |
| 66 | + } else { |
| 67 | + decryptionErrorMsg = `decrypt error: ${(result as DecryptError).error.message}`; |
| 68 | + } |
| 69 | + const pgpBlock = this.generatePgpBlockTemplate(decryptionErrorMsg, 'not signed', this.emailBodyFromThunderbirdMail); |
| 70 | + $('body').html(pgpBlock); // xss-sanitized |
| 71 | + } |
| 72 | + } else if (this.isCleartextMsg(fullMsg)) { |
| 73 | + const message = await openpgp.readCleartextMessage({ cleartextMessage: this.emailBodyFromThunderbirdMail }); |
| 74 | + const result = await OpenPGPKey.verify(message, await ContactStore.getPubkeyInfos(undefined, signerKeys)); |
| 75 | + let verificationStatus = ''; |
| 76 | + let signedMessage = ''; |
| 77 | + if (result.match && result.content) { |
| 78 | + verificationStatus = 'signed'; |
| 79 | + signedMessage = result.content.toUtfStr(); |
| 80 | + } else if (result.error) { |
| 81 | + verificationStatus = `could not verify signature: ${result.error}`; |
| 82 | + } |
| 83 | + const pgpBlock = this.generatePgpBlockTemplate('not encrypted', verificationStatus, signedMessage); |
| 84 | + $('body').html(pgpBlock); // xss-sanitized |
| 85 | + } |
| 86 | + // todo: detached signed message via https://github.com/FlowCrypt/flowcrypt-browser/issues/5668 |
| 87 | + } |
| 88 | + } |
| 89 | + }; |
| 90 | + |
| 91 | + private generatePgpBlockTemplate = (encryptionStatus: string, verificationStatus: string, messageToRender: string): string => { |
| 92 | + return ` |
| 93 | + <div ${encryptionStatus === 'encrypted' ? 'class="pgp_secure"' : 'class="pgp_neutral"'}> |
| 94 | + <div> |
| 95 | + <div id="pgp_encryption" class="pgp_badge short ${encryptionStatus === 'encrypted' ? 'green_label' : 'red_label'}">${encryptionStatus}</div> |
| 96 | + <div id="pgp_signature" class="pgp_badge short ${verificationStatus === 'signed' ? 'green_label' : 'red_label'}">${verificationStatus}</div> |
| 97 | + </div> |
| 98 | + <div class="pgp_block"> |
| 99 | + <pre>${Xss.escape(messageToRender)}</pre> |
| 100 | + </div> |
| 101 | + </div>`; |
| 102 | + }; |
| 103 | + |
| 104 | + private isCleartextMsg = (fullMsg: messenger.messages.MessagePart): boolean => { |
| 105 | + return ( |
| 106 | + (fullMsg.headers && |
| 107 | + 'openpgp' in fullMsg.headers && |
| 108 | + fullMsg.parts && |
| 109 | + fullMsg.parts[0]?.parts?.length === 1 && |
| 110 | + fullMsg.parts[0].parts[0].contentType === 'text/plain' && |
| 111 | + this.resemblesCleartextMsg(fullMsg.parts[0].parts[0].body?.trim() || '')) || |
| 112 | + false |
| 113 | + ); |
| 114 | + }; |
| 115 | + |
| 116 | + private resemblesCleartextMsg = (body: string) => { |
| 117 | + this.emailBodyFromThunderbirdMail = body; |
| 118 | + return ( |
| 119 | + body.startsWith(PgpArmor.ARMOR_HEADER_DICT.signedMsg.begin) && |
| 120 | + body.includes(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.middle)) && |
| 121 | + body.endsWith(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.end)) |
| 122 | + ); |
| 123 | + }; |
| 124 | + |
| 125 | + private isPublicKeyEncryptedMsg = (fullMsg: messenger.messages.MessagePart): boolean => { |
| 126 | + if (fullMsg.headers && 'openpgp' in fullMsg.headers && fullMsg.parts) { |
| 127 | + return ( |
| 128 | + (fullMsg.parts[0]?.parts?.length === 2 && |
| 129 | + fullMsg.parts[0]?.parts[1].contentType === 'application/pgp-encrypted' && |
| 130 | + this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) || |
| 131 | + (fullMsg.parts[0]?.parts?.length === 1 && |
| 132 | + fullMsg.parts[0]?.contentType === 'multipart/mixed' && |
| 133 | + this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) || |
| 134 | + (fullMsg.parts.length === 1 && fullMsg.parts[0]?.contentType === 'text/plain' && this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.body?.trim() || '')) || |
| 135 | + false |
| 136 | + ); |
| 137 | + } |
| 138 | + return false; |
| 139 | + }; |
| 140 | + |
| 141 | + private resemblesAsciiArmoredMsg = (body: string): boolean => { |
| 142 | + this.emailBodyFromThunderbirdMail = body; |
| 143 | + return body.startsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.begin) && body.endsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.end as string); |
| 144 | + }; |
13 | 145 | } |
0 commit comments