Added: GnuPG encrypt (sign still fails)

This commit is contained in:
the-djmaze 2022-02-09 10:53:55 +01:00
parent f3717815e1
commit 8fff6fa759
11 changed files with 196 additions and 91 deletions

View file

@ -150,12 +150,16 @@ export const GnuPGUserStore = new class {
return length && length === count; return length && length === count;
} }
getPrivateKeyFor(query, sign) { getPublicKeyFingerprints(recipients) {
return findGnuPGKey(this.privateKeys, query, sign); const fingerprints = [];
recipients.forEach(email => {
fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
});
return fingerprints;
} }
getPublicKeyFor(query, sign) { getPrivateKeyFor(query, sign) {
return findGnuPGKey(this.publicKeys, query, sign); return findGnuPGKey(this.privateKeys, query, sign);
} }
async decrypt(message) { async decrypt(message) {
@ -207,12 +211,8 @@ export const GnuPGUserStore = new class {
} }
} }
async sign(/*text, privateKey, detached*/) { async sign(privateKey) {
throw 'Sign failed'; return await askPassphrase(privateKey);
}
async encrypt(/*text, recipients, signPrivateKey*/) {
throw 'Encrypt failed';
} }
}; };

View file

@ -161,10 +161,6 @@ export const OpenPGPUserStore = new class {
return findOpenPGPKey(this.privateKeys, query/*, sign*/); 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 * https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys
*/ */

View file

@ -91,12 +91,12 @@ export const PgpUserStore = new class {
async hasPublicKeyForEmails(recipients) { async hasPublicKeyForEmails(recipients) {
const count = recipients.length; const count = recipients.length;
if (count) { if (count) {
if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) {
return 'openpgp';
}
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) { if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
return 'gnupg'; return 'gnupg';
} }
if (OpenPGPUserStore.hasPublicKeyForEmails(recipients)) {
return 'openpgp';
}
} }
return false; return false;
} }
@ -114,7 +114,7 @@ export const PgpUserStore = new class {
* Returns the first library that can. * Returns the first library that can.
*/ */
async getKeyForSigning(email) { async getKeyForSigning(email) {
/* /* // TODO: sign in PHP fails
let key = GnuPGUserStore.getPrivateKeyFor(email, 1); let key = GnuPGUserStore.getPrivateKeyFor(email, 1);
if (key) { if (key) {
return ['gnupg', key]; return ['gnupg', key];

View file

@ -30,6 +30,7 @@ import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp'; import { PgpUserStore } from 'Stores/User/Pgp';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -407,15 +408,12 @@ class ComposePopupView extends AbstractViewPopup {
params.Encrypted = draft params.Encrypted = draft
? await this.mailvelope.createDraft() ? await this.mailvelope.createDraft()
: await this.mailvelope.encrypt(recipients); : await this.mailvelope.encrypt(recipients);
} else if (encrypt || sign) { } else if ('openpgp' == encrypt || (sign && 'openpgp' == sign[0])) {
let data = new MimePart; let data = new MimePart;
data.headers['Content-Type'] = 'text/'+(TextIsHtml?'html':'plain')+'; charset="utf-8"'; data.headers['Content-Type'] = 'text/'+(TextIsHtml?'html':'plain')+'; charset="utf-8"';
data.headers['Content-Transfer-Encoding'] = 'base64'; data.headers['Content-Transfer-Encoding'] = 'base64';
data.body = base64_encode(Text); data.body = base64_encode(Text);
if (sign && sign[1]) { if (sign && sign[1]) {
if ('openpgp' != sign[0]) {
throw 'Signing with ' + sign[0] + ' not yet implemented';
}
let signed = new MimePart; let signed = new MimePart;
signed.headers['Content-Type'] = signed.headers['Content-Type'] =
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"'; 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
@ -429,13 +427,26 @@ class ComposePopupView extends AbstractViewPopup {
data = signed; data = signed;
} }
if (encrypt) { if (encrypt) {
if ('openpgp' != encrypt) {
throw 'Encryption with ' + encrypt + ' not yet implemented';
}
params.Encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients); params.Encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
} else { } else {
params.Signed = data.toString(); 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 { } else {
params.Html = TextIsHtml ? Text : ''; params.Html = TextIsHtml ? Text : '';
params.Text = TextIsHtml ? '' : Text; params.Text = TextIsHtml ? '' : Text;

View file

@ -45,7 +45,7 @@ class TempFile
public function stream_open(string $sPath) : bool public function stream_open(string $sPath) : bool
{ {
$bResult = false; $bResult = false;
$aPath = parse_url($sPath); $aPath = \parse_url($sPath);
if (isset($aPath['host']) && isset($aPath['scheme']) && if (isset($aPath['host']) && isset($aPath['scheme']) &&
\strlen($aPath['host']) && \strlen($aPath['scheme']) && \strlen($aPath['host']) && \strlen($aPath['scheme']) &&
@ -53,7 +53,7 @@ class TempFile
{ {
$sHashName = $aPath['host']; $sHashName = $aPath['host'];
if (isset(self::$aStreams[$sHashName]) && if (isset(self::$aStreams[$sHashName]) &&
is_resource(self::$aStreams[$sHashName])) \is_resource(self::$aStreams[$sHashName]))
{ {
$this->rSream = self::$aStreams[$sHashName]; $this->rSream = self::$aStreams[$sHashName];
\fseek($this->rSream, 0); \fseek($this->rSream, 0);
@ -61,7 +61,7 @@ class TempFile
} }
else else
{ {
$this->rSream = fopen('php://memory', 'r+b'); $this->rSream = \fopen('php://memory', 'r+b');
self::$aStreams[$sHashName] = $this->rSream; self::$aStreams[$sHashName] = $this->rSream;
$bResult = true; $bResult = true;
@ -78,37 +78,37 @@ class TempFile
public function stream_flush() : bool public function stream_flush() : bool
{ {
return fflush($this->rSream); return \fflush($this->rSream);
} }
public function stream_read(int $iLen) : string public function stream_read(int $iLen) : string
{ {
return fread($this->rSream, $iLen); return \fread($this->rSream, $iLen);
} }
public function stream_write(string $sInputString) : int public function stream_write(string $sInputString) : int
{ {
return fwrite($this->rSream, $sInputString); return \fwrite($this->rSream, $sInputString);
} }
public function stream_tell() : int public function stream_tell() : int
{ {
return ftell($this->rSream); return \ftell($this->rSream);
} }
public function stream_eof() : bool public function stream_eof() : bool
{ {
return feof($this->rSream); return \feof($this->rSream);
} }
public function stream_stat() : array public function stream_stat() : array
{ {
return fstat($this->rSream); return \fstat($this->rSream);
} }
public function stream_seek(int $iOffset, int $iWhence = SEEK_SET) : int public function stream_seek(int $iOffset, int $iWhence = SEEK_SET) : int
{ {
return fseek($this->rSream, $iOffset, $iWhence); return \fseek($this->rSream, $iOffset, $iWhence);
} }
} }

View file

@ -156,7 +156,7 @@ class Header
private function wordWrapHelper(string $sValue, string $sGlue = "\r\n ") : string 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 74, $sGlue
), \strlen($this->NameWithDelimitrom()))); ), \strlen($this->NameWithDelimitrom())));
} }

View file

@ -331,16 +331,13 @@ class Message extends Part
(\MailSo\Base\Utils::FunctionExistsAndEnabled('getmypid') ? \getmypid() : '')).'@'.$sHostName.'>'; (\MailSo\Base\Utils::FunctionExistsAndEnabled('getmypid') ? \getmypid() : '')).'@'.$sHostName.'>';
} }
/** public function GetRootPart() : Part
* @return resource|bool
*/
public function ToStream(bool $bWithoutBcc = false)
{ {
if (!\count($this->SubParts)) { if (!\count($this->SubParts)) {
if ($this->bAddEmptyTextPart) { if ($this->bAddEmptyTextPart) {
$oPart = new Part; $oPart = new Part;
$oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"'); $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"');
$oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(''); $oPart->Body = '';
$this->SubParts->append($oPart); $this->SubParts->append($oPart);
} else { } else {
$aAttachments = $this->oAttachmentCollection->getArrayCopy(); $aAttachments = $this->oAttachmentCollection->getArrayCopy();
@ -373,7 +370,7 @@ class Message extends Part
} }
} }
if (!\is_resource($oPart->Body)) { if (!\is_resource($oPart->Body)) {
$oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(''); $oPart->Body = '';
} }
$this->SubParts->append($oPart); $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 * 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 public function ToString(bool $bWithoutBcc = false) : string

View file

@ -32,6 +32,11 @@ class Part
*/ */
public $Body = null; public $Body = null;
/**
* @var resource
*/
public $Raw = null;
/** /**
* @var PartCollection * @var PartCollection
*/ */
@ -166,36 +171,43 @@ class Part
*/ */
public function ToStream() public function ToStream()
{ {
if ($this->SubParts->count()) { if ($this->Raw) {
$sBoundary = $this->HeaderBoundary(); $aSubStreams = array(
if (!\strlen($sBoundary)) { $this->Raw
$this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE)->setParameter( );
Enumerations\Parameter::BOUNDARY, } else {
$this->SubParts->Boundary() if ($this->SubParts->count()) {
); $sBoundary = $this->HeaderBoundary();
} else { if (!\strlen($sBoundary)) {
$this->SubParts->SetBoundary($sBoundary); $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE)->setParameter(
} Enumerations\Parameter::BOUNDARY,
} $this->SubParts->Boundary()
);
$aSubStreams = array( } else {
$this->Headers . "\r\n\r\n" $this->SubParts->SetBoundary($sBoundary);
);
if ($this->Body) {
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()) { $aSubStreams = array(
$rSubPartsStream = $this->SubParts->ToStream(); $this->Headers . "\r\n"
if (\is_resource($rSubPartsStream)) { );
$aSubStreams[] = $rSubPartsStream;
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;
}
} }
} }

View file

@ -54,7 +54,7 @@ class PartCollection extends \MailSo\Base\Collection
$aResult[] = "\r\n--{$this->sBoundary}\r\n"; $aResult[] = "\r\n--{$this->sBoundary}\r\n";
$aResult[] = $oPart->ToStream(); $aResult[] = $oPart->ToStream();
} }
$aResult[] = "\r\n--{$this->sBoundary}--\r\n"; $aResult[] = "\r\n--{$this->sBoundary}--";
return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aResult); return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aResult);
} }
return null; return null;

View file

@ -161,10 +161,6 @@ trait Messages
$this->Plugins()->RunHook('filter.send-message', array($oMessage)); $this->Plugins()->RunHook('filter.send-message', array($oMessage));
/*
TODO: PGP encrypt/sign
*/
$mResult = false; $mResult = false;
try 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)); $this->Plugins()->RunHook('filter.build-message', array($oMessage));
return $oMessage; return $oMessage;

View file

@ -2,6 +2,10 @@
namespace SnappyMail\PGP; 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 class GnuPG
{ {
private private
@ -193,24 +197,12 @@ class GnuPG
: $this->GPG->encryptFile($filename); : $this->GPG->encryptFile($filename);
} }
/** public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
* Encrypts and signs a given text
*/
public function encryptSign(string $plaintext) /*: string|false*/
{ {
\rewind($fp);
return $this->GnuPG return $this->GnuPG
? $this->GnuPG->encryptsign($plaintext) ? $this->GnuPG->encrypt(\stream_get_contents($fp))
: $this->GPG->encryptsign($plaintext); : $this->GPG->encryptStream($fp);
}
/**
* 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);
} }
/** /**
@ -360,6 +352,17 @@ class GnuPG
: $this->GPG->signFile($filename); : $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 * Verifies a signed text
*/ */