Handle S/MIME encrypt, decrypt, sign and very everywhere #259

This commit is contained in:
the-djmaze 2024-02-20 16:03:22 +01:00
parent 44a623543f
commit 5a2d2fd0e5
7 changed files with 85 additions and 48 deletions

View file

@ -35,7 +35,7 @@ import { AccountUserStore } from 'Stores/User/Account';
import { ContactUserStore } from 'Stores/User/Contact'; import { ContactUserStore } from 'Stores/User/Contact';
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 { SMimeUserStore } from 'Stores/User/SMime'; //import { SMimeUserStore } from 'Stores/User/SMime';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore, initThemes } from 'Stores/Theme'; import { ThemeStore, initThemes } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
@ -217,7 +217,8 @@ export class AppUser extends AbstractApp {
setInterval(reloadTime, 60000); setInterval(reloadTime, 60000);
PgpUserStore.init(); PgpUserStore.init();
SMimeUserStore.loadCertificates(); // TODO:
// SMimeUserStore.loadCertificates();
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500); setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);

View file

@ -12,7 +12,6 @@ import { EmailModel } from 'Model/Email';
*/ */
export function MimeToMessage(data, message) export function MimeToMessage(data, message)
{ {
let signed;
const struct = ParseMime(data); const struct = ParseMime(data);
if (struct.headers) { if (struct.headers) {
let html = struct.getByContentType('text/html'), let html = struct.getByContentType('text/html'),
@ -65,12 +64,21 @@ export function MimeToMessage(data, message)
} else { } else {
message.attachments.push(attachment); message.attachments.push(attachment);
} }
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) { } else if ('multipart/signed' === type.value) {
signed = { let protocol = type.params.protocol;
micAlg: type.micalg, if ('application/pgp-signature' === protocol) {
bodyPart: part.parts[0], message.pgpSigned({
sigPart: part.parts[1] micAlg: type.micalg,
}; bodyPart: part.parts[0],
sigPart: part.parts[1]
});
} else if ('application/pkcs7-signature' === protocol.replace('x-')) {
message.smimeSigned({
micAlg: type.micalg,
bodyPart: part,
detached: true
});
}
} }
}); });
@ -81,10 +89,9 @@ export function MimeToMessage(data, message)
message.plain(data); message.plain(data);
} }
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) { if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
signed = true; message.pgpSigned(true);
} }
message.pgpSigned(signed);
// TODO: Verify instantly? // TODO: Verify instantly?
} }

View file

@ -33,6 +33,7 @@ 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 { GnuPGUserStore } from 'Stores/User/GnuPG';
//import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport'; //import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { SMimeUserStore } from 'Stores/User/SMime';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -377,21 +378,21 @@ export class ComposePopupView extends AbstractViewPopup {
}); });
this.pgpSignKey(result) this.pgpSignKey(result)
}); });
this.initPgpEncrypt(); this.initEncrypt();
}, },
cc: value => { cc: value => {
if (false === this.showCc() && value.length) { if (false === this.showCc() && value.length) {
this.showCc(true); this.showCc(true);
} }
this.initPgpEncrypt(); this.initEncrypt();
}, },
bcc: value => { bcc: value => {
if (false === this.showBcc() && value.length) { if (false === this.showBcc() && value.length) {
this.showBcc(true); this.showBcc(true);
} }
this.initPgpEncrypt(); this.initEncrypt();
}, },
replyTo: value => { replyTo: value => {
@ -410,7 +411,7 @@ export class ComposePopupView extends AbstractViewPopup {
if (this.emptyToError() && value.length) { if (this.emptyToError() && value.length) {
this.emptyToError(false); this.emptyToError(false);
} }
this.initPgpEncrypt(); this.initEncrypt();
}, },
attachmentsInProcess: value => { attachmentsInProcess: value => {
@ -1387,7 +1388,7 @@ export class ComposePopupView extends AbstractViewPopup {
].join(',').split(',').map(value => getEmail(value.trim())).validUnique(); ].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
} }
initPgpEncrypt() { initEncrypt() {
const recipients = this.allRecipients(); const recipients = this.allRecipients();
PgpUserStore.hasPublicKeyForEmails(recipients).then(result => { PgpUserStore.hasPublicKeyForEmails(recipients).then(result => {
console.log({canPgpEncrypt:result}); console.log({canPgpEncrypt:result});
@ -1401,6 +1402,14 @@ export class ComposePopupView extends AbstractViewPopup {
// this.dropMailvelope(); // this.dropMailvelope();
} }
}); });
const count = recipients.length,
identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null,
length = count ? recipients.filter(email =>
email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length : 0;
this.canSMimeEncrypt(length && length === count);
} }
async getMessageRequestParams(sSaveFolder, draft) async getMessageRequestParams(sSaveFolder, draft)
@ -1561,12 +1570,13 @@ export class ComposePopupView extends AbstractViewPopup {
// Does encrypt attachments // Does encrypt attachments
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients)); params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
autocrypt(); autocrypt();
/* } else if (this.canSMimeEncrypt() && identity && identity.smimeKey() && identity.smimeCertificate()) {
} else if (identity && identity.smimeCertificate()) {
// TODO: S/MIME certificates of all recipients
params.encryptCertificates = [identity.smimeCertificate()]; params.encryptCertificates = [identity.smimeCertificate()];
} SMimeUserStore.forEach(certificate => {
*/ certificate.emailAddress != identity.email()
&& recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id)
});
} else { } else {
throw 'Encryption with ' + encrypt + ' not yet implemented'; throw 'Encryption with ' + encrypt + ' not yet implemented';
} }

View file

@ -1194,6 +1194,17 @@ trait Messages
$oMessage->Attachments()->Clear(); $oMessage->Attachments()->Clear();
$SMIME = $this->SMIME(); $SMIME = $this->SMIME();
$certificates = $SMIME->certificates();
// Load certificates by id
foreach ($aCertificates as &$sCertificate) {
if (!\str_contains($sCertificate, '-----BEGIN CERTIFICATE-----')) {
foreach ($certificates as $certificate) {
if ($certificate['id'] === $sCertificate) {
$sCertificate = $SMIME->getCertificate($certificate['file']);
}
}
}
}
$sEncrypted = $SMIME->encrypt($tmp, $aCertificates); $sEncrypted = $SMIME->encrypt($tmp, $aCertificates);
$oPart = new MimePart; $oPart = new MimePart;

View file

@ -114,34 +114,37 @@ trait SMime
public function DoSMimeVerifyMessage() : array public function DoSMimeVerifyMessage() : array
{ {
$sFolderName = $this->GetActionParam('folder', ''); $sBody = $this->GetActionParam('bodyPart', '');
$iUid = (int) $this->GetActionParam('uid', 0);
$sPartId = $this->GetActionParam('partId', '');
$sMicAlg = $this->GetActionParam('micAlg', '');
$bDetached = !empty($this->GetActionParam('detached', 0)); $bDetached = !empty($this->GetActionParam('detached', 0));
if (!$sBody) {
$sFolderName = $this->GetActionParam('folder', '');
$iUid = (int) $this->GetActionParam('uid', 0);
$sPartId = $this->GetActionParam('partId', '');
$sMicAlg = $this->GetActionParam('micAlg', '');
$this->initMailClientConnection(); $this->initMailClientConnection();
$oImapClient = $this->ImapClient(); $oImapClient = $this->ImapClient();
$oImapClient->FolderExamine($sFolderName); $oImapClient->FolderExamine($sFolderName);
if ('TEXT' === $sPartId) { if ('TEXT' === $sPartId) {
$oFetchResponse = $oImapClient->Fetch([ $oFetchResponse = $oImapClient->Fetch([
FetchType::BODY_PEEK.'['.$sPartId.']', FetchType::BODY_PEEK.'['.$sPartId.']',
// An empty section specification refers to the entire message, including the header. // 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]. // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME].
FetchType::BODY_HEADER_PEEK FetchType::BODY_HEADER_PEEK
], $iUid, true)[0]; ], $iUid, true)[0];
$sBody = $oFetchResponse->GetFetchValue(FetchType::BODY_HEADER); $sBody = $oFetchResponse->GetFetchValue(FetchType::BODY_HEADER);
} else { } else {
$oFetchResponse = $oImapClient->Fetch([ $oFetchResponse = $oImapClient->Fetch([
FetchType::BODY_PEEK.'['.$sPartId.']', FetchType::BODY_PEEK.'['.$sPartId.']',
// An empty section specification refers to the entire message, including the header. // 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]. // 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.'['.$sPartId.'.MIME]'
], $iUid, true)[0]; ], $iUid, true)[0];
$sBody = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]'); $sBody = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]');
}
$sBody .= $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']');
} }
$sBody .= $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']');
$result = $this->SMIME()->verify($sBody, null, !$bDetached); $result = $this->SMIME()->verify($sBody, null, !$bDetached);

View file

@ -25,6 +25,11 @@ class OpenSSL
$this->homedir = $homedir; $this->homedir = $homedir;
} }
public function getCertificate(string $filename)/* : string*/
{
return \file_get_contents("{$this->homedir}/{$filename}");
}
public function certificates() : array public function certificates() : array
{ {
$cacheFile = "{$this->homedir}/certificates.json"; $cacheFile = "{$this->homedir}/certificates.json";

View file

@ -123,6 +123,7 @@
<div id="mailvelope-settings" style="height:40em" data-bind="visible: canMailvelope"></div> <div id="mailvelope-settings" style="height:40em" data-bind="visible: canMailvelope"></div>
</details> </details>
<!-- TODO
<details> <details>
<summary class="legend">S/MIME Certificates</summary> <summary class="legend">S/MIME Certificates</summary>
<table class="table table-hover list-table"> <table class="table table-hover list-table">
@ -137,7 +138,6 @@
<span data-i18n="CRYPTO/VALID_UNTIL"></span>: <span data-i18n="CRYPTO/VALID_UNTIL"></span>:
<time data-time-format="FULL" data-bind="time:validTo_time_t"></time> <time data-time-format="FULL" data-bind="time:validTo_time_t"></time>
</td> </td>
<!--
<td> <td>
<a class="btn btn-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete()}, click: remove" <a class="btn btn-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete()}, click: remove"
data-i18n="GLOBAL/ARE_YOU_SURE"></a> data-i18n="GLOBAL/ARE_YOU_SURE"></a>
@ -145,8 +145,8 @@
<td> <td>
<span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span> <span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td> </td>
-->
</tr> </tr>
</tbody> </tbody>
</table> </table>
</details> </details>
-->