diff --git a/dev/Stores/User/GnuPG.js b/dev/Stores/User/GnuPG.js index f8954a91a..0d5e7e54d 100644 --- a/dev/Stores/User/GnuPG.js +++ b/dev/Stores/User/GnuPG.js @@ -150,12 +150,16 @@ export const GnuPGUserStore = new class { return length && length === count; } - getPrivateKeyFor(query, sign) { - return findGnuPGKey(this.privateKeys, query, sign); + getPublicKeyFingerprints(recipients) { + const fingerprints = []; + recipients.forEach(email => { + fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint); + }); + return fingerprints; } - getPublicKeyFor(query, sign) { - return findGnuPGKey(this.publicKeys, query, sign); + getPrivateKeyFor(query, sign) { + return findGnuPGKey(this.privateKeys, query, sign); } async decrypt(message) { @@ -207,12 +211,8 @@ export const GnuPGUserStore = new class { } } - async sign(/*text, privateKey, detached*/) { - throw 'Sign failed'; - } - - async encrypt(/*text, recipients, signPrivateKey*/) { - throw 'Encrypt failed'; + async sign(privateKey) { + return await askPassphrase(privateKey); } }; diff --git a/dev/Stores/User/OpenPGP.js b/dev/Stores/User/OpenPGP.js index 4f8cfdf34..6382df2e2 100644 --- a/dev/Stores/User/OpenPGP.js +++ b/dev/Stores/User/OpenPGP.js @@ -161,10 +161,6 @@ export const OpenPGPUserStore = new class { return findOpenPGPKey(this.privateKeys, query/*, sign*/); } - getPublicKeyFor(query/*, sign*/) { - return findOpenPGPKey(this.publicKeys, query/*, sign*/); - } - /** * https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys */ diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index 28b8c1be8..797c2ee34 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -91,12 +91,12 @@ export const PgpUserStore = new class { async hasPublicKeyForEmails(recipients) { const count = recipients.length; if (count) { - if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) { - return 'openpgp'; - } if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) { return 'gnupg'; } + if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) { + return 'openpgp'; + } } return false; } @@ -114,7 +114,7 @@ export const PgpUserStore = new class { * Returns the first library that can. */ async getKeyForSigning(email) { -/* +/* // TODO: sign in PHP fails let key = GnuPGUserStore.getPrivateKeyFor(email, 1); if (key) { return ['gnupg', key]; diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 580281c11..ced3a3eb4 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -30,6 +30,7 @@ import { AccountUserStore } from 'Stores/User/Account'; import { FolderUserStore } from 'Stores/User/Folder'; import { PgpUserStore } from 'Stores/User/Pgp'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; +import { GnuPGUserStore } from 'Stores/User/GnuPG'; import { MessageUserStore } from 'Stores/User/Message'; import Remote from 'Remote/User/Fetch'; @@ -407,15 +408,12 @@ class ComposePopupView extends AbstractViewPopup { params.Encrypted = draft ? await this.mailvelope.createDraft() : await this.mailvelope.encrypt(recipients); - } else if (encrypt || sign) { + } else if ('openpgp' == encrypt || (sign && 'openpgp' == sign[0])) { let data = new MimePart; data.headers['Content-Type'] = 'text/'+(TextIsHtml?'html':'plain')+'; charset="utf-8"'; data.headers['Content-Transfer-Encoding'] = 'base64'; data.body = base64_encode(Text); if (sign && sign[1]) { - if ('openpgp' != sign[0]) { - throw 'Signing with ' + sign[0] + ' not yet implemented'; - } let signed = new MimePart; signed.headers['Content-Type'] = 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"'; @@ -429,13 +427,26 @@ class ComposePopupView extends AbstractViewPopup { data = signed; } if (encrypt) { - if ('openpgp' != encrypt) { - throw 'Encryption with ' + encrypt + ' not yet implemented'; - } params.Encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients); } else { params.Signed = data.toString(); } + } else if ('gnupg' == encrypt || (sign && 'gnupg' == sign[0])) { + params.Html = TextIsHtml ? Text : ''; + params.Text = TextIsHtml ? '' : Text; +/* // TODO: sign in PHP fails + if (sign) { + params.SignFingerprint = sign[1].fingerprint; + params.SignPassphrase = await GnuPGUserStore.sign(sign[1]); + } +*/ + if (encrypt) { + params.EncryptFingerprints = GnuPGUserStore.getPublicKeyFingerprints(recipients).join(','); + } + } else if (encrypt) { + throw 'Encryption with ' + encrypt + ' not yet implemented'; + } else if (sign) { + throw 'Signing with ' + sign[0] + ' not yet implemented'; } else { params.Html = TextIsHtml ? Text : ''; params.Text = TextIsHtml ? '' : Text; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php index 04852c23f..421895140 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php @@ -45,7 +45,7 @@ class TempFile public function stream_open(string $sPath) : bool { $bResult = false; - $aPath = parse_url($sPath); + $aPath = \parse_url($sPath); if (isset($aPath['host']) && isset($aPath['scheme']) && \strlen($aPath['host']) && \strlen($aPath['scheme']) && @@ -53,7 +53,7 @@ class TempFile { $sHashName = $aPath['host']; if (isset(self::$aStreams[$sHashName]) && - is_resource(self::$aStreams[$sHashName])) + \is_resource(self::$aStreams[$sHashName])) { $this->rSream = self::$aStreams[$sHashName]; \fseek($this->rSream, 0); @@ -61,7 +61,7 @@ class TempFile } else { - $this->rSream = fopen('php://memory', 'r+b'); + $this->rSream = \fopen('php://memory', 'r+b'); self::$aStreams[$sHashName] = $this->rSream; $bResult = true; @@ -78,37 +78,37 @@ class TempFile public function stream_flush() : bool { - return fflush($this->rSream); + return \fflush($this->rSream); } public function stream_read(int $iLen) : string { - return fread($this->rSream, $iLen); + return \fread($this->rSream, $iLen); } public function stream_write(string $sInputString) : int { - return fwrite($this->rSream, $sInputString); + return \fwrite($this->rSream, $sInputString); } public function stream_tell() : int { - return ftell($this->rSream); + return \ftell($this->rSream); } public function stream_eof() : bool { - return feof($this->rSream); + return \feof($this->rSream); } public function stream_stat() : array { - return fstat($this->rSream); + return \fstat($this->rSream); } public function stream_seek(int $iOffset, int $iWhence = SEEK_SET) : int { - return fseek($this->rSream, $iOffset, $iWhence); + return \fseek($this->rSream, $iOffset, $iWhence); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php index 5f28dd8a4..a0f58dc29 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php @@ -156,7 +156,7 @@ class Header private function wordWrapHelper(string $sValue, string $sGlue = "\r\n ") : string { - return \trim(substr(wordwrap($this->NameWithDelimitrom().$sValue, + return \trim(\substr(\wordwrap($this->NameWithDelimitrom().$sValue, 74, $sGlue ), \strlen($this->NameWithDelimitrom()))); } 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 900096198..0981b4748 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 @@ -331,16 +331,13 @@ class Message extends Part (\MailSo\Base\Utils::FunctionExistsAndEnabled('getmypid') ? \getmypid() : '')).'@'.$sHostName.'>'; } - /** - * @return resource|bool - */ - public function ToStream(bool $bWithoutBcc = false) + public function GetRootPart() : Part { if (!\count($this->SubParts)) { if ($this->bAddEmptyTextPart) { $oPart = new Part; $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"'); - $oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(''); + $oPart->Body = ''; $this->SubParts->append($oPart); } else { $aAttachments = $this->oAttachmentCollection->getArrayCopy(); @@ -373,7 +370,7 @@ class Message extends Part } } if (!\is_resource($oPart->Body)) { - $oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(''); + $oPart->Body = ''; } $this->SubParts->append($oPart); @@ -425,6 +422,16 @@ class Message extends Part } } + return $oRootPart; + } + + /** + * @return resource|bool + */ + public function ToStream(bool $bWithoutBcc = false) + { + $oRootPart = $this->GetRootPart(); + /** * setDefaultHeaders */ @@ -455,7 +462,9 @@ class Message extends Part } } - return $oRootPart->ToStream(); + $resource = $oRootPart->ToStream(); + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + return $resource; } /* public function ToString(bool $bWithoutBcc = false) : string diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php index 50ac0e4ad..014c9008b 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -32,6 +32,11 @@ class Part */ public $Body = null; + /** + * @var resource + */ + public $Raw = null; + /** * @var PartCollection */ @@ -166,36 +171,43 @@ class Part */ public function ToStream() { - if ($this->SubParts->count()) { - $sBoundary = $this->HeaderBoundary(); - if (!\strlen($sBoundary)) { - $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE)->setParameter( - Enumerations\Parameter::BOUNDARY, - $this->SubParts->Boundary() - ); - } else { - $this->SubParts->SetBoundary($sBoundary); - } - } - - $aSubStreams = array( - $this->Headers . "\r\n\r\n" - ); - - if ($this->Body) { - if (\is_resource($this->Body)) { - $aMeta = \stream_get_meta_data($this->Body); - if (!empty($aMeta['seekable'])) { - \rewind($this->Body); + if ($this->Raw) { + $aSubStreams = array( + $this->Raw + ); + } else { + if ($this->SubParts->count()) { + $sBoundary = $this->HeaderBoundary(); + if (!\strlen($sBoundary)) { + $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE)->setParameter( + Enumerations\Parameter::BOUNDARY, + $this->SubParts->Boundary() + ); + } else { + $this->SubParts->SetBoundary($sBoundary); } } - $aSubStreams[] = $this->Body; - } - if ($this->SubParts->count()) { - $rSubPartsStream = $this->SubParts->ToStream(); - if (\is_resource($rSubPartsStream)) { - $aSubStreams[] = $rSubPartsStream; + $aSubStreams = array( + $this->Headers . "\r\n" + ); + + if ($this->Body) { + $aSubStreams[0] .= "\r\n"; + if (\is_resource($this->Body)) { + $aMeta = \stream_get_meta_data($this->Body); + if (!empty($aMeta['seekable'])) { + \rewind($this->Body); + } + } + $aSubStreams[] = $this->Body; + } + + if ($this->SubParts->count()) { + $rSubPartsStream = $this->SubParts->ToStream(); + if (\is_resource($rSubPartsStream)) { + $aSubStreams[] = $rSubPartsStream; + } } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php index e5f7f4395..17172c8a6 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php @@ -54,7 +54,7 @@ class PartCollection extends \MailSo\Base\Collection $aResult[] = "\r\n--{$this->sBoundary}\r\n"; $aResult[] = $oPart->ToStream(); } - $aResult[] = "\r\n--{$this->sBoundary}--\r\n"; + $aResult[] = "\r\n--{$this->sBoundary}--"; return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aResult); } return null; 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 d7d8d2129..b43e8c473 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 @@ -161,10 +161,6 @@ trait Messages $this->Plugins()->RunHook('filter.send-message', array($oMessage)); -/* - TODO: PGP encrypt/sign -*/ - $mResult = false; try { @@ -1269,6 +1265,84 @@ trait Messages } } +/* + // TODO: sign, but verify is still invalid + $sFingerprint = $this->GetActionParam('SignFingerprint', ''); + $sPassphrase = $this->GetActionParam('SignPassphrase', ''); + if ($sFingerprint) { + $GPG = $this->GnuPG(); + $oBody = $oMessage->GetRootPart(); + $fp = \fopen('php://memory', 'r+b'); + $resource = $oBody->ToStream(); +// \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + \stream_copy_to_stream($resource, $fp); + $GPG->addSignKey($sFingerprint, $sPassphrase); + $GPG->setsignmode(GNUPG_SIG_MODE_DETACH); + $sSignature = $GPG->signStream($fp); + + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $oPart = new MimePart; + $oPart->Headers->AddByName( + \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' + ); + $oMessage->SubParts->append($oPart); + + \rewind($fp); + $oBody->Raw = $fp; + $oBody->Body = null; + $oBody->SubParts->Clear(); + $oPart->SubParts->append($oBody); + + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oAlternativePart->Body = $sSignature; + $oPart->SubParts->append($oAlternativePart); + } +*/ + + // TODO: encrypt + $sFingerprints = $this->GetActionParam('EncryptFingerprints', ''); + if ($sFingerprints) { + $GPG = $this->GnuPG(); + $oBody = $oMessage->GetRootPart(); + $fp = \fopen('php://memory', 'r+b'); + $resource = $oBody->ToStream(); +// \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + \stream_copy_to_stream($resource, $fp); + foreach (\explode(',', $sFingerprints) as $sFingerprint) { + $GPG->addEncryptKey($sFingerprint); + } + $sEncrypted = $GPG->encryptStream($fp); + + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $oPart = new MimePart; + $oPart->Headers->AddByName( + \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + 'multipart/encrypted; protocol="application/pgp-encrypted"' + ); + $oMessage->SubParts->append($oPart); + + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); + $oPart->SubParts->append($oAlternativePart); + + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); + $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\R/', "\r\n", \trim($sEncrypted))); + $oPart->SubParts->append($oAlternativePart); + } + $this->Plugins()->RunHook('filter.build-message', array($oMessage)); return $oMessage; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php index 894180a58..aff693462 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php @@ -2,6 +2,10 @@ namespace SnappyMail\PGP; +defined('GNUPG_SIG_MODE_NORMAL') || define('GNUPG_SIG_MODE_NORMAL', 0); +defined('GNUPG_SIG_MODE_DETACH') || define('GNUPG_SIG_MODE_DETACH', 1); +defined('GNUPG_SIG_MODE_CLEAR') || define('GNUPG_SIG_MODE_CLEAR', 2); + class GnuPG { private @@ -193,24 +197,12 @@ class GnuPG : $this->GPG->encryptFile($filename); } - /** - * Encrypts and signs a given text - */ - public function encryptSign(string $plaintext) /*: string|false*/ + public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/ { + \rewind($fp); return $this->GnuPG - ? $this->GnuPG->encryptsign($plaintext) - : $this->GPG->encryptsign($plaintext); - } - - /** - * Encrypts and signs a given text - */ - public function encryptSignFile(string $filename) /*: string|false*/ - { - return $this->GnuPG - ? $this->GnuPG->encryptsign(\file_get_contents($filename)) - : $this->GPG->encryptsignFile($filename); + ? $this->GnuPG->encrypt(\stream_get_contents($fp)) + : $this->GPG->encryptStream($fp); } /** @@ -360,6 +352,17 @@ class GnuPG : $this->GPG->signFile($filename); } + /** + * Signs a given file + */ + public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/ + { + \rewind($fp); + return $this->GnuPG + ? $this->GnuPG->sign(\stream_get_contents($fp)) + : $this->GPG->signStream($fp); + } + /** * Verifies a signed text */