From 73fa215e22b885cc2071d0929a6aeefe15af6dc2 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 3 Feb 2022 10:17:18 +0100 Subject: [PATCH] #89 OpenPGP.js encrypt (+ verify) --- dev/Stores/User/OpenPGP.js | 44 ++++++++++++-- dev/Stores/User/Pgp.js | 8 +-- dev/View/Popup/Compose.js | 58 +++++++++++++++---- dev/View/User/MailBox/MessageView.js | 7 +++ .../MailSo/Mime/Enumerations/MimeType.php | 6 ++ .../app/libraries/MailSo/Mime/Message.php | 24 +------- .../libraries/RainLoop/Actions/Messages.php | 32 ++++++---- 7 files changed, 126 insertions(+), 53 deletions(-) diff --git a/dev/Stores/User/OpenPGP.js b/dev/Stores/User/OpenPGP.js index 5c3b907bc..8f4f8fb78 100644 --- a/dev/Stores/User/OpenPGP.js +++ b/dev/Stores/User/OpenPGP.js @@ -184,6 +184,7 @@ export const OpenPGPUserStore = new class { const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]); if (null !== passphrase) { const + publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/), decryptedKey = await openpgp.decryptKey({ privateKey: privateKey.key, passphrase @@ -191,7 +192,7 @@ export const OpenPGPUserStore = new class { return await openpgp.decrypt({ message, - verificationKeys: this.getPublicKeyFor(sender), + verificationKeys: publicKey && publicKey.key, // expectSigned: true, // signature: '', // Detached signature decryptionKeys: decryptedKey @@ -238,21 +239,52 @@ export const OpenPGPUserStore = new class { } } - async signCleartext(text, privateKey) { + /** + * https://docs.openpgpjs.org/global.html#sign + */ + async sign(text, privateKey, detached) { const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]); if (null !== passphrase) { privateKey = await openpgp.decryptKey({ privateKey: privateKey.key, passphrase }); - const unsignedMessage = await openpgp.createCleartextMessage({ text: text }); + const message = detached + ? await openpgp.createMessage({ text: text }) + : await openpgp.createCleartextMessage({ text: text }); return await openpgp.sign({ - message: unsignedMessage, // CleartextMessage or Message object - signingKeys: privateKey -// detached: false + message: message, + signingKeys: privateKey, + detached: !!detached }); } return false; } + /** + * https://docs.openpgpjs.org/global.html#encrypt + */ + async encrypt(text, recipients, signPrivateKey) { + const count = recipients.length; + recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key); + if (count === recipients.length) { + if (signPrivateKey) { + const passphrase = prompt('OpenPGP.js Passphrase for ' + signPrivateKey.id + ' ' + signPrivateKey.emails[0]); + if (null === passphrase) { + return; + } + signPrivateKey = await openpgp.decryptKey({ + privateKey: signPrivateKey.key, + passphrase + }); + } + return await openpgp.encrypt({ + message: await openpgp.createMessage({ text: text }), + encryptionKeys: recipients.map(pkey => pkey.key), + signingKeys: signPrivateKey +// signature + }); + } + } + }; diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index ce9e128b8..3a02f0d11 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -74,14 +74,14 @@ export const PgpUserStore = new class { async hasPublicKeyForEmails(recipients, all) { const count = recipients.length; if (count) { - if (GnuPGUserStore.hasPublicKeyForEmails(recipients, all)) { - return 'gnupg'; - } - if (OpenPGPUserStore.hasPublicKeyForEmails(recipients, all)) { return 'openpgp'; } + if (GnuPGUserStore.hasPublicKeyForEmails(recipients, all)) { + return 'gnupg'; + } + let keyring = this.mailvelopeKeyring, mailvelope = keyring && await keyring.validKeyForAddress(recipients) /*.then(LookupResult => Object.entries(LookupResult))*/; diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index b229939ad..22dd803ed 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -360,8 +360,8 @@ class ComposePopupView extends AbstractViewPopup { Bcc: this.bcc(), ReplyTo: this.replyTo(), Subject: this.subject(), - TextIsHtml: TextIsHtml ? 1 : 0, - Text: Text, + Html: TextIsHtml ? Text : '', + Text: TextIsHtml ? '' : Text, DraftInfo: this.aDraftInfo, InReplyTo: this.sInReplyTo, References: this.sReferences, @@ -453,16 +453,16 @@ class ComposePopupView extends AbstractViewPopup { ); if (encrypt) { - throw 'Encryption not yet implemented'; - } - if (sign && 'openpgp' != sign[0]) { - throw 'Signing with ' + sign[0] + ' not yet implemented'; - } - if (sign && 'openpgp' == sign[0]) { - if (params.TextIsHtml) { - throw i18n('PGP_NOTIFICATIONS/PGP_ERROR', { ERROR: "Can't sign HTML" }); + if (params.Html) { + throw 'Encrypt HTML with ' + encrypt + ' not yet implemented'; } - OpenPGPUserStore.signCleartext(params.Text, sign[1]).then(text => { + if ('openpgp' != encrypt) { + throw 'Encryption with ' + encrypt + ' not yet implemented'; + } + if (sign && 'openpgp' != sign[0]) { + throw 'Signing with ' + sign[0] + ' not yet implemented'; + } + OpenPGPUserStore.encrypt(params.Text, this.allRecipients(), sign && sign[1]).then(text => { if (text) { params.Text = text; send(); @@ -472,6 +472,42 @@ class ComposePopupView extends AbstractViewPopup { this.sending(false); } }); + } else if (sign) { + if (params.Html) { + throw 'Signing HTML with ' + sign[0] + ' not yet implemented'; + } + if ('openpgp' != sign[0]) { + throw 'Signing with ' + sign[0] + ' not yet implemented'; + } + const detached = false; + if (detached) { + // Append headers + params.Text = [ + 'Content-Transfer-Encoding: base64', + 'Content-Type: text/plain; charset="utf-8"; protected-headers="v1"', +// 'From: Demo ', +// 'To: Demo ', +// 'Subject: text detached signed' + '' + ] + // Now the body in base64 + .concat(btoa(params.Text).match(/.{1,76}/g)) + .join("\r\n"); + } + OpenPGPUserStore.sign(params.Text, sign[1]).then(text => { + if (text) { + if (detached) { + params.Signature = text; + } else { + params.Text = text; + } + send(); + } else { + this.sendError(true); + this.sendErrorDesc(i18n('PGP_NOTIFICATIONS/PGP_ERROR', { ERROR: 'Signing failed' })); + this.sending(false); + } + }); } else { send(); } diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 6104f4d57..7ea130f7c 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -634,6 +634,13 @@ export class MailMessageView extends AbstractViewRight { PgpUserStore.decrypt(oMessage).then(result => { if (result && result.data) { mimeToMessage(result.data, oMessage); + if (result.signatures) { + oMessage.pgpSigned(true); + oMessage.pgpVerified({ + signatures: result.signatures, + success: !!result.signatures.length + }); + } } }); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php index ae9bdf5a3..f1fe30d97 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php @@ -24,7 +24,13 @@ abstract class MimeType const MULTIPART_ALTERNATIVE = 'multipart/alternative'; const MULTIPART_RELATED = 'multipart/related'; const MULTIPART_MIXED = 'multipart/mixed'; + const MULTIPART_SIGNED = 'multipart/signed'; + const APPLICATION_PGP_SIGNATURE = 'application/pgp-signature'; + + const MULTIPART_ENCRYPTED = 'multipart/encrypted'; + const APPLICATION_PGP_ENCRYPTED = 'application/pgp-encrypted'; + const APPLICATION_OCTET_STREAM = 'application/octet-stream'; const MESSAGE_RFC822 = 'message/rfc822'; const MESSAGE_PARTIAL = 'message/partial'; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php index 59ddff35b..2390e214d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php @@ -47,28 +47,15 @@ class Message $this->oAttachmentCollection = new AttachmentCollection; } - public function DoesNotCreateEmptyTextPart() : self - { - $this->bAddEmptyTextPart = false; - - return $this; - } - - public function DoesNotAddDefaultXMailer() : self + public function DoesNotAddDefaultXMailer() : void { $this->bAddDefaultXMailer = false; - - return $this; } public function MessageId() : string { - $sResult = ''; - if (!empty($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) - { - $sResult = $this->aHeadersValue[Enumerations\Header::MESSAGE_ID]; - } - return $sResult; + return empty($this->aHeadersValue[Enumerations\Header::MESSAGE_ID]) ? '' + : $this->aHeadersValue[Enumerations\Header::MESSAGE_ID]; } public function SetMessageId(string $sMessageId) : void @@ -316,11 +303,6 @@ class Message return $this->AddAlternative(Enumerations\MimeType::TEXT_HTML, $sHtml); } - public function AddText(string $sHtmlOrPlainText, bool $bIsHtml = false) : self - { - return $bIsHtml ? $this->AddHtml($sHtmlOrPlainText) : $this->AddPlain($sHtmlOrPlainText); - } - public function AddAlternative(string $sContentType, string $sData) : self { $this->aAlternativeParts[] = array( diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index e2141e366..ae0f96375 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -160,6 +160,10 @@ trait Messages $this->Plugins()->RunHook('filter.send-message', array($oMessage)); +/* + TODO: PGP encrypt/sign +*/ + $mResult = false; try { @@ -995,7 +999,7 @@ trait Messages $this->Plugins()->RunHook('filter.read-receipt-message-plain', array($oAccount, $oMessage, &$sText)); - $oMessage->AddText($sText, false); + $oMessage->AddPlain($sText); $this->Plugins()->RunHook('filter.build-read-receipt-message', array($oMessage, $oAccount)); @@ -1004,8 +1008,6 @@ trait Messages private function buildMessage(Account $oAccount, bool $bWithDraftInfo = true) : \MailSo\Mime\Message { - $bTextIsHtml = '1' === $this->GetActionParam('TextIsHtml', '0'); - $oMessage = new \MailSo\Mime\Message(); if (!$this->Config()->Get('security', 'hide_x_mailer_header', true)) @@ -1084,18 +1086,26 @@ trait Messages $aFoundDataURL = array(); $aFoundContentLocationUrls = array(); + $sHtml = $this->GetActionParam('Html', ''); $sText = $this->GetActionParam('Text', ''); - if ($bTextIsHtml) { - $sText = \MailSo\Base\HtmlUtils::BuildHtml($sText, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls); - } - $this->Plugins()->RunHook($bTextIsHtml ? 'filter.message-html' : 'filter.message-plain', - array($oAccount, $oMessage, &$sText)); - if ($bTextIsHtml && \strlen($sText)) { + if ($sHtml) { + $sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls); + $this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml)); $sTextConverted = \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sText); $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sTextConverted)); - $oMessage->AddText($sTextConverted, false); + $oMessage->AddPlain($sTextConverted); + $oMessage->AddHtml($sText); + } else { + $sSignature = $this->GetActionParam('Signature', null); + if ($sSignature) { + // MimeType::MULTIPART_SIGNED + // MimeType::APPLICATION_PGP_SIGNATURE + $oMessage->AddPlain($sText); + } else { + $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sText)); + $oMessage->AddPlain($sText); + } } - $oMessage->AddText($sText, $bTextIsHtml); $aAttachments = $this->GetActionParam('Attachments', null); if (\is_array($aAttachments))