Skip to content

send password-protected message through FES and update email format #1254

Description

@tomholub

When sending password protected message, we must upload it to FES & format the Gmail message differently. You can study EncryptedMsgMailFormatter on browser extension for details not mentioned here.

The steps are (when sending a password-protected message through FES:

  • get a message token (reply token) from FES
  • encode a string that contains sender, recipient, subject, reply token, as follows: replyInfo = Str.base64urlUtfEncode(JSON.stringify({sender: "...", recipient: ["..."], subject: "...", token: "..."}))
  • format a div html element that contains . infoDiv = '<div style="display: none" class="cryptup_reply" cryptup-data="REPLY INFO STRING"></div>'
  • append the div to plaintext as follows: bodyWithReplyToken = newMsgData.plaintext + '\n\n' + infoDiv
  • construct a regular plain mime message using the above plain text + attachments: pgpMimeWithAttachments = Mime.encode(bodyWithReplyToken, { Subject: newMsg.subject }, attachments);
  • encrypt the encoded plain mime message ONLY FOR THE MESSAGE PASSWORD, no public keys. pwdEncryptedWithAttachments = encryptDataArmor(pgpMimeWithAttachments, newMsg.pwd, []); // encrypted only for pwd, not signed. This message will be uploaded, therefore it's only encrypted for message password.
  • upload resulting data to FES. msgUrl = this.view.acctServer.messageUpload(idToken, pwdEncryptedWithAttachments, replyToken, from, recipients, { renderUploadProgress(p, PercentageAccounting.FIRST_HALF) } ). Notice that the upload progress will be divided by two, so that instead of rendering 0-100%, we render 0-50% at this stage.
  • encrypt and format the final Gmail message. First encrypt body: encryptedTextFile = Core.encryptFile(newMsgData.plaintext, publicKeys) // no attachments, no infoDiv, no mime. Use public keys to encrypt as usual, as available. No password
  • then encrypt attachments encryptedAttachments = plainAttachments.map { Core.encryptFile($0, publicKeys) } // add .pgp to name if not done automatically
  • put all attachments into an array encryptedAttachmentsAndBodyAsFiles = [Attachment(data=encryptedTextFile, name=message.asc, mimeType=application/pgp-encrypted)] + encryptedAttachments
  • format message text to go into the email, which is in plain text. See method formatPwdEncryptedMsgBodyLink. emailAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(msgUrl); - NOTE: skip the intro part from the linked code, we are not implementing this on mobile yet
  • now all data is encrypted manually. The password-encrypted one was uploaded to FES and we have URL, which was encoded into emailAndLinkBody. Attachments were encrypted separately. Compose it all into a plain message together: sendableMessageMime = Core.composeEmail(format=plain, emailAndLinkBody, encryptedAttachmentsAndBodyAsFiles)
  • finally you have mime data to send. Upload to gmail & use PercentageAccounting.SECOND_HALF, eg on browser extension (simplified):
    // if this is a password-encrypted message, then we've already shown progress for uploading to backend
    // and this requests represents second half of uploadable effort. Else this represents all (no previous heavy requests)
    const progressRepresents = isPasswordEncryptedMessage ? 'SECOND-HALF' : 'EVERYTHING';`
    msgSentRes = await this.view.emailProvider.msgSend(msg, (p) => this.renderUploadProgress(p, progressRepresents));

You can start implementation with approximately this method:

  private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (
    authInfo: FcUuidAuth, newMsgData: NewMsgData
  ): Promise<{ bodyWithReplyToken: SendableMsgBody, replyToken: string }> => {
    const recipients = Array.prototype.concat.apply([], Object.values(newMsgData.recipients));
    try {
      const response = await this.view.acctServer.messageToken(authInfo);
      const infoDiv = Ui.e('div', {
        'style': 'display: none;',
        'class': 'cryptup_reply',
        'cryptup-data': Str.htmlAttrEncode({
          sender: newMsgData.from,
          recipient: Value.arr.withoutVal(Value.arr.withoutVal(recipients, newMsgData.from), this.acctEmail),
          subject: newMsgData.subject,
          token: response.replyToken,
        })
      });
      return {
        bodyWithReplyToken: { 'text/plain': newMsgData.plaintext + '\n\n' + infoDiv, 'text/html': newMsgData.plainhtml + '<br /><br />' + infoDiv },
        replyToken: response.replyToken
      };
    } catch (msgTokenErr) {
      if (ApiErr.isAuthErr(msgTokenErr)) {
        Settings.offerToLoginWithPopupShowModalOnErr(this.acctEmail);
        throw new ComposerResetBtnTrigger();
      } else if (ApiErr.isNetErr(msgTokenErr)) {
        throw msgTokenErr;
      }
      // note - you don't need to re-implement this exactly
      throw Catch.rewrapErr(msgTokenErr, 'There was a token error sending this message. Please try again. Let us know at human@flowcrypt.com if this happens repeatedly.');
    }
  };

Rendering upload progress:

  public renderUploadProgress = (progress: number | undefined, progressRepresents: 'FIRST-HALF' | 'SECOND-HALF' | 'EVERYTHING') => {
    if (progress && this.view.attachmentsModule.attachment.hasAttachment()) {
      if (progressRepresents === 'FIRST-HALF') {
        progress = Math.floor(progress / 2); // show 0-50% instead of 0-100%
      } else if (progressRepresents === 'SECOND-HALF') {
        progress = Math.floor(50 + progress / 2); // show 50-100% instead of 0-100%
      } else {
        progress = Math.floor(progress); // show 0-100%
      }
      this.view.S.now('send_btn_text').text(`${SendBtnTexts.BTN_SENDING} ${progress < 100 ? `${progress}%` : ''}`);
    }
  };

Formatting plain part of Gmail message

  private formatPwdEncryptedMsgBodyLink = async (msgUrl: string): Promise<SendableMsgBody> => {
    const storage = await AcctStore.get(this.acctEmail, ['outgoing_language']);
    const lang = storage.outgoing_language || 'EN';
    const aStyle = `padding: 2px 6px; background: #2199e8; color: #fff; display: inline-block; text-decoration: none;`;
    const a = `<a href="${Xss.escape(msgUrl)}" style="${aStyle}">${Lang.compose.openMsg[lang]}</a>`;
    const intro = this.view.S.cached('input_intro').length ? this.view.inputModule.extract('text', 'input_intro') : undefined;
    const text = [];
    const html = [];
    if (intro) {
      text.push(intro + '\n');
      html.push(Xss.escape(intro).replace(/\n/g, '<br>') + '<br><br>');
    }
    const senderEmail = Xss.escape(this.view.senderModule.getSender());
    text.push(Lang.compose.msgEncryptedText(lang, senderEmail) + msgUrl + '\n\n');
    html.push(`${Lang.compose.msgEncryptedHtml(lang, senderEmail) + a}<br/><br/>${Lang.compose.alternativelyCopyPaste[lang] + Xss.escape(msgUrl)}<br/><br/>`);
    return { 'text/plain': text.join('\n'), 'text/html': html.join('\n') };
  };

Formatting final Gmail message:

  private sendablePwdMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], msgUrl: string, signingPrv?: Key) => {
    // encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only)
    const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext };
    const pgpMimeNoAttachments = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no attachments, attached to email separately
    const { data: pubEncryptedNoAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAttachments), undefined, pubs, signingPrv); // encrypted only for pubs
    const attachments = this.createPgpMimeAttachments(pubEncryptedNoAttachments).
      concat(await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubs)); // encrypted only for pubs
    const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(msgUrl);
    return await SendableMsg.createPwdMsg(this.acctEmail, this.headers(newMsg), emailIntroAndLinkBody, attachments, { isDraft: this.isDraft });
  };

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions