Automatically verify PGP and S/MIME signed messages

This commit is contained in:
the-djmaze 2024-03-04 01:07:00 +01:00
parent 27fbe01876
commit 870019d2df
8 changed files with 82 additions and 47 deletions

View file

@ -120,12 +120,10 @@ export class MessageModel extends AbstractModel {
encrypted: false, encrypted: false,
pgpSigned: null, pgpSigned: null,
pgpVerified: null,
pgpEncrypted: null, pgpEncrypted: null,
pgpDecrypted: false, pgpDecrypted: false,
smimeSigned: null, smimeSigned: null,
smimeVerified: null,
smimeEncrypted: null, smimeEncrypted: null,
smimeDecrypted: false, smimeDecrypted: false,
@ -199,10 +197,9 @@ export class MessageModel extends AbstractModel {
} }
}); });
this.smimeSigned.subscribe(value => { this.smimeSigned.subscribe(value =>
value?.body && MimeToMessage(value.body, this); value?.body && MimeToMessage(value.body, this)
'verified' in value && this.smimeVerified(value.verified); );
});
} }
get requestHash() { get requestHash() {

View file

@ -583,8 +583,7 @@ export class MailMessageView extends AbstractViewRight {
MimeToMessage(result.data, oMessage); MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain(); oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures?.length) { if (result.signatures?.length) {
oMessage.pgpSigned(true); oMessage.pgpSigned({
oMessage.pgpVerified({
signatures: result.signatures, signatures: result.signatures,
success: !!result.signatures.length success: !!result.signatures.length
}); });
@ -603,7 +602,7 @@ export class MailMessageView extends AbstractViewRight {
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/; const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
PgpUserStore.verify(oMessage).then(result => { PgpUserStore.verify(oMessage).then(result => {
if (result) { if (result) {
oMessage.pgpVerified(result); oMessage.pgpSigned(result);
} else { } else {
alert('Verification failed or no valid public key found'); alert('Verification failed or no valid public key found');
} }
@ -668,22 +667,22 @@ export class MailMessageView extends AbstractViewRight {
} }
smimeVerify(/*self, event*/) { smimeVerify(/*self, event*/) {
const message = currentMessage(); const message = currentMessage(),
let data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" } data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
if (data) { if (data) {
data = { ...data }; // clone const params = { ...data }; // clone
data.folder = message.folder; params.folder = message.folder;
data.uid = message.uid; params.uid = message.uid;
data.bodyPart = data.bodyPart?.raw; params.bodyPart = data.bodyPart?.raw;
data.sigPart = data.sigPart?.bodyRaw; params.sigPart = data.sigPart?.bodyRaw;
Remote.post('SMimeVerifyMessage', null, data).then(response => { Remote.post('SMimeVerifyMessage', null, params).then(response => {
if (response?.Result) { if (response?.Result) {
if (response.Result.body) { if (response.Result.body) {
MimeToMessage(response.Result.body, message); MimeToMessage(response.Result.body, message);
message.html() ? message.viewHtml() : message.viewPlain(); message.html() ? message.viewHtml() : message.viewPlain();
response.Result.body = null;
} }
message.smimeVerified(response.Result.success); data.success = response.Result.success;
message.smimeSigned(data);
} }
}); });
} }

View file

@ -126,7 +126,6 @@ class MailClient
$this->oImapClient->FolderExamine($sFolderName); $this->oImapClient->FolderExamine($sFolderName);
$oBodyStructure = null; $oBodyStructure = null;
$oMessage = null;
$aFetchItems = array( $aFetchItems = array(
FetchType::UID, FetchType::UID,
@ -169,28 +168,10 @@ class MailClient
} }
$aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid); $aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid);
if (\count($aFetchResponse)) {
$oMessage = Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure);
// S/MIME signed. Verify it, so we have the raw mime body to show
if ($oMessage->smimeSigned) try {
$bOpaque = !$oMessage->smimeSigned['detached'];
$sBody = $this->oImapClient->FetchMessagePart(
$oMessage->Uid,
$oMessage->smimeSigned['partId']
);
$result = (new \SnappyMail\SMime\OpenSSL(''))->verify($sBody, null, $bOpaque);
if ($result) {
if ($bOpaque) {
$oMessage->smimeSigned['body'] = $result['body'];
}
$oMessage->smimeSigned['verified'] = $result['success'];
}
} catch (\Throwable $e) {
$this->logException($e);
}
}
return $oMessage; return \count($aFetchResponse)
? Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure)
: null;
} }
/** /**

View file

@ -66,7 +66,7 @@ class Message implements \JsonSerializable
private ?array $DraftInfo = null; private ?array $DraftInfo = null;
private ?array $pgpSigned = null; public ?array $pgpSigned = null;
private ?array $pgpEncrypted = null; private ?array $pgpEncrypted = null;
public ?array $smimeSigned = null; public ?array $smimeSigned = null;

View file

@ -7,6 +7,7 @@ use RainLoop\Exceptions\ClientException;
use RainLoop\Model\Account; use RainLoop\Model\Account;
use RainLoop\Notifications; use RainLoop\Notifications;
use MailSo\Imap\SequenceSet; use MailSo\Imap\SequenceSet;
use MailSo\Imap\Enumerations\FetchType;
use MailSo\Imap\Enumerations\MessageFlag; use MailSo\Imap\Enumerations\MessageFlag;
use MailSo\Mime\Part as MimePart; use MailSo\Mime\Part as MimePart;
use MailSo\Mime\Enumerations\Header as MimeEnumHeader; use MailSo\Mime\Enumerations\Header as MimeEnumHeader;
@ -455,6 +456,63 @@ trait Messages
try try
{ {
$oMessage = $this->MailClient()->Message($sFolder, $iUid, true, $this->Cacher($oAccount)); $oMessage = $this->MailClient()->Message($sFolder, $iUid, true, $this->Cacher($oAccount));
// S/MIME signed. Verify it, so we have the raw mime body to show
if ($oMessage->smimeSigned) try {
$bOpaque = !$oMessage->smimeSigned['detached'];
$sBody = $this->ImapClient()->FetchMessagePart(
$oMessage->Uid,
$oMessage->smimeSigned['partId']
);
$result = (new \SnappyMail\SMime\OpenSSL(''))->verify($sBody, null, $bOpaque);
if ($result) {
if ($bOpaque) {
$oMessage->smimeSigned['body'] = $result['body'];
}
$oMessage->smimeSigned['success'] = $result['success'];
}
} catch (\Throwable $e) {
$this->logException($e);
}
if ($oMessage->pgpSigned) try {
$GPG = $this->GnuPG();
if ($GPG) {
if ($oMessage->pgpSigned['sigPartId']) {
$sPartId = $oMessage->pgpSigned['partId'];
$sSigPartId = $oMessage->pgpSigned['sigPartId'];
$aParts = [
FetchType::BODY_PEEK.'['.$sPartId.']',
// 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.'['.$sPartId.'.MIME]',
FetchType::BODY_PEEK.'['.$sSigPartId.']'
];
$oFetchResponse = $this->ImapClient()->Fetch($aParts, $oMessage->Uid, true)[0];
$sBodyMime = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]');
$info = $this->GnuPG()->verify(
\preg_replace('/\\r?\\n/su', "\r\n",
$sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']')
),
\preg_replace('/[^\x00-\x7F]/', '',
$oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']')
)
);
} else {
// clearsigned text
$info = $this->GnuPG()->verify($oMessage->sPlain, '');
}
if (!empty($info[0]) && 0 == $info[0]['status']) {
$info = $info[0];
$oMessage->pgpSigned = [
'fingerprint' => $info['fingerprint'],
'success' => true
];
}
}
} catch (\Throwable $e) {
$this->logException($e);
}
} }
catch (\Throwable $oException) catch (\Throwable $oException)
{ {

View file

@ -329,7 +329,7 @@ trait Pgp
'text' => \preg_replace('/\\r?\\n/su', "\r\n", 'text' => \preg_replace('/\\r?\\n/su', "\r\n",
$sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']') $sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']')
), ),
'signature' => preg_replace('/[^\x00-\x7F]/', '', 'signature' => \preg_replace('/[^\x00-\x7F]/', '',
$oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']') $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']')
) )
]; ];

View file

@ -15,7 +15,7 @@ abstract class GnuPG
return GPG::isSupported(); return GPG::isSupported();
} }
private static $instance; private static $instance = null;
public static function getInstance(string $homedir) : ?PGPInterface public static function getInstance(string $homedir) : ?PGPInterface
{ {
if (!static::$instance) { if (!static::$instance) {

View file

@ -300,10 +300,10 @@
</div> </div>
</div> </div>
<div data-bind="visible: message().pgpSigned()"> <div data-bind="visible: message().pgpSigned()">
<div class="crypto-control signed" data-bind="css: {success: message().pgpVerified()?.success, error: message().pgpVerified() && !message().pgpVerified().success}"> <div class="crypto-control signed" data-bind="css: {success: true === message().pgpSigned()?.success, error: false === message().pgpSigned()?.success}">
<span data-icon="✍" data-i18n="OPENPGP/SIGNED_MESSAGE"></span> <span data-icon="✍" data-i18n="OPENPGP/SIGNED_MESSAGE"></span>
<button class="btn" data-bind="visible: pgpSupported, click: pgpVerify" data-i18n="CRYPTO/VERIFY"></button> <button class="btn" data-bind="visible: pgpSupported, click: pgpVerify" data-i18n="CRYPTO/VERIFY"></button>
<div data-icon="⚠" data-bind="visible: message().pgpVerified()?.error, text: message().pgpVerified()?.error"></div> <div data-icon="⚠" data-bind="visible: message().pgpSigned()?.error, text: message().pgpSigned()?.error"></div>
</div> </div>
</div> </div>
@ -315,7 +315,7 @@
</div> </div>
</div> </div>
<div data-bind="visible: message().smimeSigned()"> <div data-bind="visible: message().smimeSigned()">
<div class="crypto-control signed" data-bind="css: {success: true === message().smimeVerified(), error: false === message().smimeVerified()}"> <div class="crypto-control signed" data-bind="css: {success: true === message().smimeSigned()?.success, error: false === message().smimeSigned()?.success}">
<span data-icon="✍" data-i18n="SMIME/SIGNED_MESSAGE"></span> <span data-icon="✍" data-i18n="SMIME/SIGNED_MESSAGE"></span>
<button class="btn" data-bind="click: smimeVerify" data-i18n="CRYPTO/VERIFY"></button> <button class="btn" data-bind="click: smimeVerify" data-i18n="CRYPTO/VERIFY"></button>
</div> </div>