Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 53
allow signed and encrypted S/MIME message#4074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
65967bfa122c528fd6a5d5d34d6a1518cad8c03a4b3b9c830a33443016149572016085ba24571784d5470b50b302d566f1a0eb9226966fbde9057cad9e65c6File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -13,7 +13,7 @@ import { ComposerUserError } from './compose-err-module.js'; | ||
| import { ComposeSendBtnPopoverModule } from './compose-send-btn-popover-module.js'; | ||
| import { GeneralMailFormatter } from './formatters/general-mail-formatter.js'; | ||
| import { GmailRes } from '../../../js/common/api/email-provider/gmail/gmail-parser.js'; | ||
| import { KeyInfo, Key, KeyUtil } from '../../../js/common/core/crypto/key.js'; | ||
| import { KeyInfo } from '../../../js/common/core/crypto/key.js'; | ||
| import { SendBtnTexts } from './compose-types.js'; | ||
| import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js'; | ||
| import { Str } from '../../../js/common/core/common.js'; | ||
| @@ -112,18 +112,12 @@ export class ComposeSendBtnModule extends ViewModule<ComposeView> { | ||
| this.view.S.cached('send_btn_note').text(''); | ||
| const newMsgData = this.view.inputModule.extractAll(); | ||
| await this.view.errModule.throwIfFormValsInvalid(newMsgData); | ||
| const senderKi = await this.view.storageModule.getKey(this.view.senderModule.getSender()); | ||
| let signingPrv: Key | undefined; | ||
| if (this.popover.choices.sign) { | ||
| signingPrv = await this.decryptSenderKey(senderKi); | ||
| if (!signingPrv) { | ||
| return; // user has canceled the pass phrase dialog, or didn't respond to it in time | ||
| } | ||
| } | ||
| await ContactStore.update(undefined, Array.prototype.concat.apply([], Object.values(newMsgData.recipients)), { lastUse: Date.now() }); | ||
| const msgObj = await GeneralMailFormatter.processNewMsg(this.view, newMsgData, senderKi, signingPrv); | ||
| await this.finalizeSendableMsg(msgObj, senderKi); | ||
| await this.doSendMsg(msgObj); | ||
| const msgObj = await GeneralMailFormatter.processNewMsg(this.view, newMsgData); | ||
| if (msgObj) { | ||
| await this.finalizeSendableMsg(msgObj); | ||
| await this.doSendMsg(msgObj.msg); | ||
| } | ||
| } catch (e) { | ||
| await this.view.errModule.handleSendErr(e); | ||
| } finally { | ||
| @@ -132,7 +126,7 @@ export class ComposeSendBtnModule extends ViewModule<ComposeView> { | ||
| } | ||
| } | ||
| private finalizeSendableMsg = async (msg: SendableMsg, senderKi: KeyInfo) => { | ||
| private finalizeSendableMsg = async ({ msg, senderKi }: { msg: SendableMsg, senderKi: KeyInfo | undefined }) => { | ||
| const choices = this.view.sendBtnModule.popover.choices; | ||
| for (const k of Object.keys(this.additionalMsgHeaders)) { | ||
| msg.headers[k] = this.additionalMsgHeaders[k]; | ||
| @@ -149,7 +143,7 @@ export class ComposeSendBtnModule extends ViewModule<ComposeView> { | ||
| msg.body['text/html'] = htmlWithCidImages; | ||
| msg.attachments.push(...imgAttachments); | ||
| } | ||
| if (this.view.myPubkeyModule.shouldAttach()) { | ||
| if (this.view.myPubkeyModule.shouldAttach() && senderKi) { // todo: report on undefined? | ||
| msg.attachments.push(Attachment.keyinfoAsPubkeyAttachment(senderKi)); | ||
| } | ||
Comment on lines
152
to
148
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think ok to let it be as is in this case, as attaching public key to email is not as crucial. | ||
| await this.addNamesToMsg(msg); | ||
| @@ -230,25 +224,6 @@ export class ComposeSendBtnModule extends ViewModule<ComposeView> { | ||
| } | ||
| } | ||
| private decryptSenderKey = async (senderKi: KeyInfo): Promise<Key | undefined> => { | ||
| const prv = await KeyUtil.parse(senderKi.private); | ||
| const passphrase = await this.view.storageModule.passphraseGet(senderKi); | ||
| if (typeof passphrase === 'undefined' && !prv.fullyDecrypted) { | ||
| BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'sign', longids: [senderKi.longid] }); | ||
| if ((typeof await this.view.storageModule.whenMasterPassphraseEntered(60)) !== 'undefined') { // pass phrase entered | ||
| return await this.decryptSenderKey(senderKi); | ||
| } else { // timeout - reset - no passphrase entered | ||
| this.resetSendBtn(); | ||
| return undefined; | ||
| } | ||
| } else { | ||
| if (!prv.fullyDecrypted) { | ||
| await KeyUtil.decrypt(prv, passphrase!); // checked !== undefined above | ||
| } | ||
| return prv; | ||
| } | ||
| } | ||
| private addNamesToMsg = async (msg: SendableMsg): Promise<void> => { | ||
| const { sendAs } = await AcctStore.get(this.view.acctEmail, ['sendAs']); | ||
| const addNameToEmail = async (emails: string[]): Promise<string[]> => { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,11 +3,11 @@ | ||
| 'use strict'; | ||
| import { Bm, BrowserMsg } from '../../../js/common/browser/browser-msg.js'; | ||
| import { KeyInfo, KeyUtil, Key } from '../../../js/common/core/crypto/key.js'; | ||
| import { KeyInfo, KeyUtil, Key, PubkeyResult } from '../../../js/common/core/crypto/key.js'; | ||
| import { ApiErr } from '../../../js/common/api/shared/api-error.js'; | ||
| import { Assert } from '../../../js/common/assert.js'; | ||
| import { Catch } from '../../../js/common/platform/catch.js'; | ||
| import { CollectPubkeysResult } from './compose-types.js'; | ||
| import { Catch, UnreportableError } from '../../../js/common/platform/catch.js'; | ||
| import { CollectKeysResult } from './compose-types.js'; | ||
| import { PUBKEY_LOOKUP_RESULT_FAIL } from './compose-err-module.js'; | ||
| import { ViewModule } from '../../../js/common/view-module.js'; | ||
| import { ComposeView } from '../compose.js'; | ||
| @@ -31,48 +31,115 @@ export class ComposeStorageModule extends ViewModule<ComposeView> { | ||
| }); | ||
| } | ||
| public getKey = async (senderEmail: string): Promise<KeyInfo> => { | ||
| const keys = await KeyStore.get(this.view.acctEmail); | ||
| let result = await this.view.myPubkeyModule.chooseMyPublicKeyBySenderEmail(keys, senderEmail); | ||
| if (!result) { | ||
| this.view.errModule.debug(`ComposerStorage.getKey: could not find key based on senderEmail: ${senderEmail}, using primary instead`); | ||
| result = keys[0]; | ||
| Assert.abortAndRenderErrorIfKeyinfoEmpty(result); | ||
| // if `type` is supplied, returns undefined if no keys of this type are found | ||
| public getKeyOptional = async (senderEmail: string | undefined, type?: 'openpgp' | 'x509' | undefined) => { | ||
| const keys = await KeyStore.getTypedKeyInfos(this.view.acctEmail); | ||
| let result: KeyInfo | undefined; | ||
| if (senderEmail !== undefined) { | ||
| const filteredKeys = KeyUtil.filterKeysByTypeAndSenderEmail(keys, senderEmail, type); | ||
| if (type === undefined) { | ||
| // prioritize openpgp | ||
| result = filteredKeys.find(key => key.type === 'openpgp'); | ||
| } | ||
| if (result === undefined) { | ||
| result = filteredKeys[0]; | ||
| } | ||
| } | ||
| if (result === undefined) { | ||
| this.view.errModule.debug(`ComposerStorage.getKeyOptional: could not find key based on senderEmail: ${senderEmail}, using primary instead`); | ||
| result = keys.find(k => type === undefined || type === k.type); | ||
| } else { | ||
| this.view.errModule.debug(`ComposerStorage.getKey: found key based on senderEmail: ${senderEmail}`); | ||
| this.view.errModule.debug(`ComposerStorage.getKeyOptional: found key based on senderEmail: ${senderEmail}`); | ||
| } | ||
| return result; | ||
| } | ||
| public getKey = async (senderEmail: string | undefined, type?: 'openpgp' | 'x509' | undefined): Promise<KeyInfo> => { | ||
| const result = await this.getKeyOptional(senderEmail, type); | ||
| Assert.abortAndRenderErrorIfKeyinfoEmpty(result); | ||
| this.view.errModule.debug(`ComposerStorage.getKey: returning key longid: ${result!.longid}`); | ||
| return result!; | ||
| } | ||
| public collectAllAvailablePublicKeys = async (senderEmail: string, senderKi: KeyInfo, recipients: string[]): Promise<CollectPubkeysResult> => { | ||
| // used when encryption is needed | ||
| // returns a set of keys of a single family ('openpgp' or 'x509') | ||
| public collectSingleFamilyKeys = async (recipients: string[], senderEmail: string, needSigning: boolean): Promise<CollectKeysResult> => { | ||
| const contacts = await ContactStore.getEncryptionKeys(undefined, recipients); | ||
| const pubkeys = [{ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }]; | ||
| const emailsWithoutPubkeys = []; | ||
| for (const contact of contacts) { | ||
| let keysPerEmail = contact.keys; | ||
| // if non-expired present, return non-expired only | ||
| if (keysPerEmail.some(k => k.usableForEncryption)) { | ||
| keysPerEmail = keysPerEmail.filter(k => k.usableForEncryption); | ||
| const resultsPerType: { [type: string]: CollectKeysResult } = {}; | ||
| const OPENPGP = 'openpgp'; | ||
| const X509 = 'x509'; | ||
| for (const i of [OPENPGP, X509]) { | ||
| const type = i as ('openpgp' | 'x509'); | ||
| // senderKi for draft encryption! | ||
| const senderKi = await this.getKeyOptional(senderEmail, type); | ||
| const { pubkeys, emailsWithoutPubkeys } = this.collectPubkeysByType(type, contacts); | ||
| if (senderKi !== undefined) { | ||
| // add own key for encryption | ||
| pubkeys.push({ pubkey: await KeyUtil.parse(senderKi.public), email: senderEmail, isMine: true }); | ||
| } | ||
| if (keysPerEmail.length) { | ||
| for (const pubkey of keysPerEmail) { | ||
| pubkeys.push({ pubkey, email: contact.email, isMine: false }); | ||
| } | ||
| } else { | ||
| emailsWithoutPubkeys.push(contact.email); | ||
| const result = { senderKi, pubkeys, emailsWithoutPubkeys }; | ||
| if (!emailsWithoutPubkeys.length && (senderKi !== undefined || !needSigning)) { | ||
| return result; // return right away | ||
| } | ||
| resultsPerType[type] = result; | ||
| } | ||
| return { pubkeys, emailsWithoutPubkeys }; | ||
| // per discussion https://github.com/FlowCrypt/flowcrypt-browser/issues/4069#issuecomment-957313631 | ||
| // if one emailsWithoutPubkeys isn't subset of the other, throw an error | ||
| if (!resultsPerType[OPENPGP].emailsWithoutPubkeys.every(email => resultsPerType[X509].emailsWithoutPubkeys.includes(email)) && | ||
| !resultsPerType[X509].emailsWithoutPubkeys.every(email => resultsPerType[OPENPGP].emailsWithoutPubkeys.includes(email))) { | ||
| let err = `Cannot use mixed OpenPGP (${resultsPerType[OPENPGP].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) and ` | ||
| + `S/MIME (${resultsPerType[X509].pubkeys.filter(p => !p.isMine).map(p => p.email).join(', ')}) public keys yet.`; | ||
| err += 'If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.'; | ||
| throw new UnreportableError(err); | ||
| } | ||
| const rank = (x: [string, CollectKeysResult]) => { | ||
| return x[1].emailsWithoutPubkeys.length * 100 + (x[1].senderKi ? 0 : 10) + (x[0] === 'openpgp' ? 0 : 1); | ||
| }; | ||
| return Object.entries(resultsPerType).sort((a, b) => rank(a) - rank(b))[0][1]; | ||
| } | ||
| public passphraseGet = async (senderKi?: KeyInfo) => { | ||
| public passphraseGet = async (senderKi?: { longid: string }) => { | ||
| if (!senderKi) { | ||
| senderKi = await KeyStore.getFirstRequired(this.view.acctEmail); | ||
| } | ||
| return await PassphraseStore.get(this.view.acctEmail, senderKi); | ||
| } | ||
Comment on lines
+101
to
106
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not so sure about how this method is implemented in general. It seems weird to just fetch the first available key if missing. Will need to look at context how this method is used, maybe it can be made clearer (in another issue, sometime later). | ||
| public decryptSenderKey = async (senderKi: KeyInfo): Promise<Key | undefined> => { | ||
| const prv = await KeyUtil.parse(senderKi.private); | ||
| const passphrase = await this.passphraseGet(senderKi); | ||
| if (typeof passphrase === 'undefined' && !prv.fullyDecrypted) { | ||
| BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'sign', longids: [senderKi.longid] }); | ||
| if ((typeof await this.whenMasterPassphraseEntered(60)) !== 'undefined') { // pass phrase entered | ||
| return await this.decryptSenderKey(senderKi); | ||
| } else { // timeout - reset - no passphrase entered | ||
| this.view.sendBtnModule.resetSendBtn(); | ||
| return undefined; | ||
| } | ||
| } else { | ||
| if (!prv.fullyDecrypted) { | ||
| await KeyUtil.decrypt(prv, passphrase!); // checked !== undefined above | ||
| } | ||
| return prv; | ||
| } | ||
| } | ||
| public isPwdMatchingPassphrase = async (pwd: string): Promise<boolean> => { | ||
| const kis = await KeyStore.get(this.view.acctEmail); | ||
| for (const ki of kis) { | ||
| const pp = await PassphraseStore.get(this.view.acctEmail, ki, true); | ||
| if (pp && pwd.toLowerCase() === pp.toLowerCase()) { | ||
| return true; | ||
| } | ||
| // check whether this pwd unlocks the ki | ||
| const parsed = await KeyUtil.parse(ki.private); | ||
| if (!parsed.fullyDecrypted && await KeyUtil.decrypt(parsed, pwd)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| public lookupPubkeyFromKeyserversThenOptionallyFetchExpiredByFingerprintAndUpsertDb = async ( | ||
| email: string, name: string | undefined | ||
| ): Promise<PubkeyInfo[] | "fail"> => { | ||
| @@ -217,4 +284,23 @@ export class ComposeStorageModule extends ViewModule<ComposeView> { | ||
| } | ||
| } | ||
| private collectPubkeysByType = (type: 'openpgp' | 'x509', contacts: { email: string, keys: Key[] }[]): { pubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] } => { | ||
| const pubkeys: PubkeyResult[] = []; | ||
| const emailsWithoutPubkeys: string[] = []; | ||
| for (const contact of contacts) { | ||
| let keysPerEmail = contact.keys.filter(k => k.type === type); | ||
| // if non-expired present, return non-expired only | ||
| if (keysPerEmail.some(k => k.usableForEncryption)) { | ||
| keysPerEmail = keysPerEmail.filter(k => k.usableForEncryption); | ||
| } | ||
| if (keysPerEmail.length) { | ||
| for (const pubkey of keysPerEmail) { | ||
| pubkeys.push({ pubkey, email: contact.email, isMine: false }); | ||
| } | ||
| } else { | ||
| emailsWithoutPubkeys.push(contact.email); | ||
| } | ||
| } | ||
| return { pubkeys, emailsWithoutPubkeys }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw this happens when there is no signing prv (pass phrase). In this situation I like to throw a specific error, like
ComposeResetBtnTriggerorComposeUserErrror, to make the intention / purpose clearer. I guessComposeResetBtnTriggerwould work here. Then the return types can be simplified.