Added GnuPG->verify() for testing #89

Bugfix SnappyMail\PGP\Keyservers
Renamed DoPgpVerify to DoMessagePgpVerify
This commit is contained in:
the-djmaze 2022-01-17 22:04:14 +01:00
parent fde44c0102
commit bf84684965
8 changed files with 134 additions and 56 deletions

View file

@ -658,7 +658,7 @@ export class MessageModel extends AbstractModel {
if (params) { if (params) {
params.Folder = this.folder; params.Folder = this.folder;
params.Uid = this.uid; params.Uid = this.uid;
rl.app.Remote.post('PgpVerify', null, params) rl.app.Remote.post('MessagePgpVerify', null, params)
.then(data => { .then(data => {
console.dir(data); console.dir(data);
}) })

View file

@ -713,7 +713,7 @@ trait Admin
'loaded' => true 'loaded' => true
] ]
]; ];
foreach (['APCu', 'cURL','GD','Gmagick','Imagick','intl','LDAP','OpenSSL','pdo_mysql','pdo_pgsql','pdo_sqlite','redis','Sodium','uuid','XXTEA','Zip'] as $name) { foreach (['APCu', 'cURL','GnuPG','GD','Gmagick','Imagick','intl','LDAP','OpenSSL','pdo_mysql','pdo_pgsql','pdo_sqlite','redis','Sodium','uuid','XXTEA','Zip'] as $name) {
$aResult[] = [ $aResult[] = [
'name' => $name, 'name' => $name,
'loaded' => \extension_loaded(\strtolower($name)) 'loaded' => \extension_loaded(\strtolower($name))

View file

@ -12,40 +12,6 @@ use MailSo\Imap\Enumerations\MessageFlag;
trait Messages trait Messages
{ {
public function DoPgpVerify() : array
{
$sFolderName = $this->GetActionParam('Folder', '');
$iUid = (int) $this->GetActionParam('Uid', 0);
$sBodyPartId = $this->GetActionParam('BodyPartId', '');
$sSigPartId = $this->GetActionParam('SigPartId', '');
$sMicAlg = $this->GetActionParam('MicAlg', '');
$oAccount = $this->initMailClientConnection();
$oImapClient = $this->MailClient()->ImapClient();
$oImapClient->FolderExamine($sFolderName);
$aFetchResponse = $oImapClient->Fetch([
// An empty section specification refers to the entire message, including the header.
// But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME].
$aFetchItems[] = FetchType::BODY_PEEK.'['.$sBodyPartId.'.MIME]',
$aFetchItems[] = FetchType::BODY_PEEK.'['.$sBodyPartId.']',
$aFetchItems[] = FetchType::BODY_PEEK.'['.$sSigPartId.']'
], $iUid, true);
$oFetchResponse = $aFetchResponse[0];
$result = [
'MIME' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.'.MIME]')),
'Body' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']')),
'Signature' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']'))
];
// TODO: check gnugp and \SnappyMail\PGP\Keyservers::get() and \SnappyMail\PGP\Keyservers::index()
return $this->DefaultResponse(__FUNCTION__, $result);
}
/** /**
* @throws \MailSo\Base\Exceptions\Exception * @throws \MailSo\Base\Exceptions\Exception
*/ */
@ -686,6 +652,93 @@ trait Messages
return $this->DefaultResponse(__FUNCTION__, $mResult); return $this->DefaultResponse(__FUNCTION__, $mResult);
} }
public function DoMessagePgpVerify() : array
{
$sFolderName = $this->GetActionParam('Folder', '');
$iUid = (int) $this->GetActionParam('Uid', 0);
$sBodyPartId = $this->GetActionParam('BodyPartId', '');
$sSigPartId = $this->GetActionParam('SigPartId', '');
$sMicAlg = $this->GetActionParam('MicAlg', '');
$oAccount = $this->initMailClientConnection();
$oImapClient = $this->MailClient()->ImapClient();
$oImapClient->FolderExamine($sFolderName);
$aFetchResponse = $oImapClient->Fetch([
// An empty section specification refers to the entire message, including the header.
// But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME].
FetchType::BODY_PEEK.'['.$sBodyPartId.'.MIME]',
FetchType::BODY_PEEK.'['.$sBodyPartId.']',
FetchType::BODY_PEEK.'['.$sSigPartId.']',
FetchType::BuildBodyCustomHeaderRequest([
\MailSo\Mime\Enumerations\Header::FROM_,
], true)
], $iUid, true);
$oFetchResponse = $aFetchResponse[0];
$sKey = '';
$sFrom = $oFetchResponse->GetFetchValue('BODY[HEADER.FIELDS (FROM)]');
$aFrom = [];
if (\preg_match('/[^\\s<>]+@[^\\s<>]+/', $sFrom, $aFrom)) {
$sFrom = $aFrom[0];
}
if ($sFrom) {
/* // Check expired/revoked
$aKey = $this->StorageProvider()->Get(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP,
\sha1($sFrom)
);
if ($aKey) {
$sKey = $aKeys[0]['key'];
} else
*/
try {
$aKeys = \SnappyMail\PGP\Keyservers::index($sFrom);
if ($aKeys) {
$sKey = \SnappyMail\PGP\Keyservers::get($aKeys[0]['keyid']);
if ($sKey) {
$aKeys[0]['key'] = $sKey;
/*
$this->StorageProvider()->Put(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP,
\sha1($sFrom),
$aKeys[0]
);
*/
}
}
} catch (\Throwable $e) {
// ignore
}
}
$result = [
'MIME' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.'.MIME]')),
'Body' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']')),
'Signature' => \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']')),
'From' => $sFrom,
'PubKey' => $sKey
];
// TODO: this fails
if ($result['PubKey'] && \class_exists('gnupg')) {
$pgp_dir = $this->StorageProvider()->GenerateFilePath($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::PGP);
$gpg = new \gnupg(['home_dir' => \dirname($pgp_dir) . '/.gnupg']);
$gpg->import($result['PubKey']);
$info = $gpg->verify(
\trim(\trim($result['MIME']) . "\r\n\r\n" . \trim($result['Body'])),
$result['Signature']
);
$result['gnupg'] = $info;
}
return $this->DefaultResponse(__FUNCTION__, $result);
}
/** /**
* @throws \RainLoop\Exceptions\ClientException * @throws \RainLoop\Exceptions\ClientException
* @throws \MailSo\Net\Exceptions\ConnectionException * @throws \MailSo\Net\Exceptions\ConnectionException

View file

@ -66,6 +66,11 @@ class Storage extends \RainLoop\Providers\AbstractProvider
return $this->oDriver->DeleteStorage($mAccount); return $this->oDriver->DeleteStorage($mAccount);
} }
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string
{
return $this->oDriver->GenerateFilePath($mAccount, $iStorageType, $bMkDir);
}
public function IsActive() : bool public function IsActive() : bool
{ {
return true; return true;

View file

@ -9,4 +9,5 @@ class StorageType
const NOBODY = 3; const NOBODY = 3;
const SIGN_ME = 4; const SIGN_ME = 4;
const SESSION = 5; const SESSION = 5;
const PGP = 6;
} }

View file

@ -89,10 +89,7 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
return $this->bLocal; return $this->bLocal;
} }
/** public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false, bool $bForDeleteAction = false) : string
* @param \RainLoop\Model\Account|string|null $mAccount
*/
protected function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string
{ {
$sEmail = $sSubFolder = ''; $sEmail = $sSubFolder = '';
if (null === $mAccount) { if (null === $mAccount) {
@ -103,6 +100,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
$sSubFolder = '.sign_me'; $sSubFolder = '.sign_me';
} else if (StorageType::SESSION === $iStorageType) { } else if (StorageType::SESSION === $iStorageType) {
$sSubFolder = '.sessions'; $sSubFolder = '.sessions';
} else if (StorageType::PGP === $iStorageType) {
$sSubFolder = '.pgp';
} }
} else if ($mAccount instanceof \RainLoop\Model\AdditionalAccount) { } else if ($mAccount instanceof \RainLoop\Model\AdditionalAccount) {
$sEmail = $mAccount->ParentEmail(); $sEmail = $mAccount->ParentEmail();
@ -117,11 +116,12 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
switch ($iStorageType) switch ($iStorageType)
{ {
case StorageType::NOBODY: case StorageType::NOBODY:
$sFilePath = $this->sDataPath.'/__nobody__/'.\sha1($sKey ?: \time()); $sFilePath = $this->sDataPath.'/__nobody__/';
break; break;
case StorageType::SIGN_ME: case StorageType::SIGN_ME:
case StorageType::SESSION: case StorageType::SESSION:
case StorageType::CONFIG: case StorageType::CONFIG:
case StorageType::PGP:
if (empty($sEmail)) { if (empty($sEmail)) {
return ''; return '';
} }
@ -133,8 +133,7 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
$sFilePath = $this->sDataPath $sFilePath = $this->sDataPath
.'/'.\RainLoop\Utils::fixName($sDomain ?: 'unknown.tld') .'/'.\RainLoop\Utils::fixName($sDomain ?: 'unknown.tld')
.'/'.\RainLoop\Utils::fixName(\implode('@', $aEmail) ?: '.unknown') .'/'.\RainLoop\Utils::fixName(\implode('@', $aEmail) ?: '.unknown')
.'/'.($sSubFolder ? \RainLoop\Utils::fixName($sSubFolder).'/' : '') .'/'.($sSubFolder ? \RainLoop\Utils::fixName($sSubFolder).'/' : '');
.($sKey ? \RainLoop\Utils::fixName($sKey) : '');
break; break;
default: default:
throw new \Exception("Invalid storage type {$iStorageType}"); throw new \Exception("Invalid storage type {$iStorageType}");
@ -151,6 +150,22 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
return $sFilePath; return $sFilePath;
} }
/**
* @param \RainLoop\Model\Account|string|null $mAccount
*/
protected function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string
{
$sFilePath = $this->GenerateFilePath($mAccount, $iStorageType, $bMkDir, $bForDeleteAction);
if ($sFilePath) {
if (StorageType::NOBODY === $iStorageType) {
$sFilePath .= \sha1($sKey ?: \time());
} else {
$sFilePath .= ($sKey ? \RainLoop\Utils::fixName($sKey) : '');
}
}
return $sFilePath;
}
public function SetLogger(?\MailSo\Log\Logger $oLogger) public function SetLogger(?\MailSo\Log\Logger $oLogger)
{ {
$this->oLogger = $oLogger; $this->oLogger = $oLogger;

View file

@ -13,22 +13,24 @@ namespace SnappyMail\PGP;
abstract class Keyservers abstract class Keyservers
{ {
public static $keyservers = [ public static $hosts = [
/* /*
'hkp://keys.gnupg.net', 'https://keys.openpgp.org',
'hkps://keyserver.ubuntu.com', 'https://pgp.mit.edu',
keys.openpgp.org 'https://keyring.debian.org',
pgp.mit.edu 'https://attester.flowcrypt.com',
keyring.debian.org 'https://zimmermann.mayfirst.org',
keyserver.ubuntu.com 'https://pool.sks-keyservers.net',
attester.flowcrypt.com
zimmermann.mayfirst.org
*/ */
'https://keys.fedoraproject.org' 'https://keyserver.ubuntu.com',
'https://keys.fedoraproject.org',
'https://keys.openpgp.org'
]; ];
private static function fetch(string $host, string $op, string $search, bool $fingerprint = false, bool $exact = false) : ?Response private static function fetch(string $host, string $op, string $search, bool $fingerprint = false, bool $exact = false) : ?\SnappyMail\HTTP\Response
{ {
$host = \str_replace('hkp://', 'http://', $host);
$host = \str_replace('hkps://', 'https://', $host);
$search = \urlencode($search); $search = \urlencode($search);
$fingerprint = $fingerprint ? '&fingerprint=on' : ''; $fingerprint = $fingerprint ? '&fingerprint=on' : '';
$exact = $exact ? '&exact=on' : ''; $exact = $exact ? '&exact=on' : '';
@ -57,7 +59,7 @@ abstract class Keyservers
$keyId = '0x' . $keyId; $keyId = '0x' . $keyId;
} }
foreach ($this->keyservers as $host) { foreach (static::$hosts as $host) {
$oResponse = static::fetch($host, 'get', $keyId); $oResponse = static::fetch($host, 'get', $keyId);
if (!$oResponse) { if (!$oResponse) {
\SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}"); \SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}");
@ -86,7 +88,7 @@ abstract class Keyservers
public static function index(string $search, bool $fingerprint = true, bool $exact = false) : array public static function index(string $search, bool $fingerprint = true, bool $exact = false) : array
{ {
$keys = []; $keys = [];
foreach ($this->keyservers as $host) { foreach (static::$hosts as $host) {
$oResponse = static::fetch($host, 'index', $search, $fingerprint, $exact); $oResponse = static::fetch($host, 'index', $search, $fingerprint, $exact);
if (!$oResponse) { if (!$oResponse) {
\SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}"); \SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}");
@ -98,6 +100,7 @@ abstract class Keyservers
} }
$result = \explode("\n", $oResponse->body); $result = \explode("\n", $oResponse->body);
$curKey = null;
foreach ($result as $line) { foreach ($result as $line) {
// https://datatracker.ietf.org/doc/html/draft-shaw-openpgp-hkp-00#section-5.2 // https://datatracker.ietf.org/doc/html/draft-shaw-openpgp-hkp-00#section-5.2
$line = \explode(':', $line); $line = \explode(':', $line);

View file

@ -13,6 +13,7 @@ if (defined('APP_VERSION'))
$aOptional = array( $aOptional = array(
'cURL' => extension_loaded('curl'), 'cURL' => extension_loaded('curl'),
'exif' => extension_loaded('exif'), 'exif' => extension_loaded('exif'),
'gnupg' => extension_loaded('gnupg'),
'gd' => extension_loaded('gd'), 'gd' => extension_loaded('gd'),
'gmagick' => extension_loaded('gmagick'), 'gmagick' => extension_loaded('gmagick'),
'imagick' => extension_loaded('imagick'), 'imagick' => extension_loaded('imagick'),