Added a S/MIME certificates list to User settings #259

This commit is contained in:
the-djmaze 2024-02-19 20:16:37 +01:00
parent a22ae5fcf4
commit 3f08051e37
8 changed files with 172 additions and 219 deletions

View file

@ -35,6 +35,7 @@ import { AccountUserStore } from 'Stores/User/Account';
import { ContactUserStore } from 'Stores/User/Contact';
import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
import { SMimeUserStore } from 'Stores/User/SMime';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore, initThemes } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language';
@ -216,6 +217,7 @@ export class AppUser extends AbstractApp {
setInterval(reloadTime, 60000);
PgpUserStore.init();
SMimeUserStore.loadCertificates();
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);

View file

@ -15,6 +15,8 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
import { SMimeUserStore } from 'Stores/User/SMime';
import Remote from 'Remote/User/Fetch';
export class UserSettingsSecurity extends AbstractViewSettings {
@ -43,6 +45,8 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
this.smimeCertificates = SMimeUserStore;
this.canOpenPGP = SettingsCapa('OpenPGP');
this.canGnuPG = GnuPGUserStore.isSupported();
this.canMailvelope = !!window.mailvelope;

17
dev/Stores/User/SMime.js Normal file
View file

@ -0,0 +1,17 @@
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
import Remote from 'Remote/User/Fetch';
export const SMimeUserStore = koArrayWithDestroy();
addObservablesTo(SMimeUserStore, {
loading: false
});
SMimeUserStore.loadCertificates = () => {
SMimeUserStore([]);
SMimeUserStore.loading(true);
Remote.request('SMimeGetCertificates', (iError, oData) => {
SMimeUserStore.loading(false);
iError || SMimeUserStore(oData.Result);
});
};

View file

@ -1124,7 +1124,7 @@ trait Messages
$detached = true;
$SMIME = new \SnappyMail\SMime\OpenSSL;
$SMIME = $this->SMIME();
$SMIME->setCertificate($sCertificate);
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
$sSignature = $SMIME->sign($tmp, $detached);
@ -1186,8 +1186,7 @@ trait Messages
$oMessage->SubParts->Clear();
$oMessage->Attachments()->Clear();
// $SMIME = new \SnappyMail\SMime(/*$homedir*/);
$SMIME = new \SnappyMail\SMime\OpenSSL;
$SMIME = $this->SMIME();
/*
foreach ($aCertificates as $sCertificate) {
$SMIME->addEncryptKey($sCertificate);

View file

@ -8,6 +8,32 @@ use MailSo\Imap\Enumerations\FetchType;
trait SMime
{
private $SMIME = null;
public function SMIME() : OpenSSL
{
if (!$this->SMIME) {
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
return null;
}
$homedir = \rtrim($this->StorageProvider()->GenerateFilePath(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::ROOT
), '/') . '/.smime';
if (!\is_dir($homedir)) {
\mkdir($homedir, 0700, true);
}
if (!\is_writable($homedir)) {
throw new \Exception("smime homedir '{$homedir}' not writable");
}
$this->SMIME = new OpenSSL($homedir);
}
return $this->SMIME;
}
public function DoGetSMimeCertificate() : array
{
$result = [
@ -18,18 +44,35 @@ trait SMime
return $this->DefaultResponse(\array_values(\array_unique($result)));
}
// Like DoGnupgGetKeys
public function DoSMimeGetCertificates() : array
{
return $this->DefaultResponse(
$this->SMIME()->certificates()
);
}
/*
DoGetPGPKeys() : array
DoPgpSearchKey() : array
DoGnupgDecrypt() : array
DoGnupgGetKeys() : array
DoGnupgExportKey() : array
DoGnupgGenerateKey() : array
DoGnupgDeleteKey() : array
DoPgpImportKey() : array
DoGetStoredPGPKeys() : array
DoPgpStoreKeyPair() : array
DoStorePGPKey() : array
DoPgpVerifyMessage() : array
*/
/**
* Can be use by Identity
*/
public function DoSMimeCreateCertificate() : array
{
$oAccount = $this->getAccountFromToken();
/*
$homedir = \rtrim($this->StorageProvider()->GenerateFilePath(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::ROOT
), '/') . '/.smime';
*/
$sName = $this->GetActionParam('name', '') ?: $oAccount->Name();
$sEmail = $this->GetActionParam('email', '') ?: $oAccount->Email();
$sPassphrase = $this->GetActionParam('passphrase', '');
@ -72,8 +115,7 @@ trait SMime
}
$sBody .= $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']');
$SMIME = new OpenSSL;
$result = $SMIME->verify($sBody, null, !$bDetached);
$result = $this->SMIME()->verify($sBody, null, !$bDetached);
return $this->DefaultResponse($result);
}

View file

@ -1,199 +0,0 @@
<?php
namespace SnappyMail;
class SMime
{
private
$homedir,
// Instance of SnappyMail\SMime\OpenSSL
$OpenSSL,
// Instance of \SnappyMail\GPG\SMIME
$GPGSM;
function __construct(string $homedir)
{
$homedir = \rtrim($homedir, '/\\');
// BSD 4.4 max length
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
throw new \Exception('socket name for S.gpg-agent.extra is too long');
}
$this->homedir = $homedir;
}
public static function isSupported() : bool
{
return SMime\OpenSSL::isSupported() || GPG\SMIME::isSupported();
}
private static $instance;
public static function getInstance(string $homedir) : ?self
{
if (!static::$instance) {
static::$instance = new self($homedir);
}
return static::$instance;
}
public function handler()
{
return $this->OpenSSL ?: $this->GPGSM;
}
public function getGPGSM(bool $throw = true) : ?GPG\SMIME
{
if (!$this->GPGSM) {
if (GPG\SMIME::isSupported()) {
$this->GPGSM = new GPG\SMIME($this->homedir);
} else if ($throw) {
throw new \Exception('GnuPG not supported');
}
}
return $this->GPGSM;
}
public function addDecryptKey(string $fingerprint,
#[\SensitiveParameter]
string $passphrase
) : bool
{
}
public function addEncryptKey(string $fingerprint) : bool
{
}
public function addSignKey(string $fingerprint,
#[\SensitiveParameter]
?string $passphrase
) : bool
{
}
public function clearDecryptKeys() : bool
{
}
public function clearEncryptKeys() : bool
{
}
public function clearSignKeys() : bool
{
}
public function decrypt(string $text) /*: string|false */
{
}
public function decryptFile(string $filename) /*: string|false */
{
}
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */
{
}
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
{
}
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
{
}
public function deleteKey(string $keyId, bool $private) : bool
{
}
public function encrypt(string $plaintext) /*: string|false*/
{
}
public function encryptFile(string $filename) /*: string|false*/
{
}
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
{
}
public function export(string $fingerprint,
#[\SensitiveParameter]
string $passphrase = ''
) /*: string|false*/
{
}
public function getEngineInfo() : array
{
}
public function getError() /*: string|false*/
{
}
public function getErrorInfo() : array
{
}
public function getProtocol() : int
{
}
public function generateKey(string $uid,
#[\SensitiveParameter]
string $passphrase
) /*: string|false*/
{
}
public function import(string $keydata) /*: array|false*/
{
}
public function importFile(string $filename) /*: array|false*/
{
}
public function keyInfo(string $pattern) : array
{
}
public function setArmor(bool $armor = true) : bool
{
}
public function setErrorMode(int $errormode) : void
{
}
public function setSignMode(int $signmode) : bool
{
}
public function sign(string $plaintext) /*: string|false*/
{
}
public function signFile(string $filename) /*: string|false*/
{
}
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
{
}
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{
}
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{
}
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: string|false */
{
}
}

View file

@ -10,6 +10,7 @@ use SnappyMail\File\Temporary;
class OpenSSL
{
private string $homedir;
private array $headers = [];
private int $flags = 0;
private int $cipher_algo = \OPENSSL_CIPHER_AES_128_CBC;
@ -19,6 +20,61 @@ class OpenSSL
private $certificate; // OpenSSLCertificate|array|string
private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
function __construct(string $homedir)
{
$this->homedir = $homedir;
}
public function certificates() : array
{
$keys = [];
foreach (\glob("{$this->homedir}/*.key") as $file) {
$data = \file_get_contents($file);
// Can't check ENCRYPTED PRIVATE KEY
if (\str_contains($data, '-----BEGIN PRIVATE KEY-----')) {
$keys[] = [\basename($file), $data];
}
}
$result = [];
foreach (\glob("{$this->homedir}/*.crt") as $file) {
$name = \basename($file);
$certificate = \file_get_contents($file);
$data = \openssl_x509_parse($certificate);
if ($data) {
$short = [
'file' => \basename($file),
'CN' => $data['subject']['CN'],
'emailAddress' => $data['subject']['emailAddress'],
'validTo' => \gmdate('Y-m-d\\TH:i:s\\Z', $data['validTo_time_t']),
'smimesign' => false,
'smimeencrypt' => false,
'privateKey' => null // not found or encrypted
];
foreach ($data['purposes'] as $purpose) {
if ('smimesign' === $purpose[2] || 'smimeencrypt' === $purpose[2]) {
// [general availability, tested purpose]
$short[$purpose[2]] = $purpose[0] || $purpose[1];
}
}
foreach ($keys as $key) {
if (\openssl_x509_check_private_key($certificate, $key[1])) {
$short['privateKey'] = $key[0];
break;
}
}
$result[] = $short;
} else {
\error_log("OpenSSL parse({$file}): " . \openssl_error_string());
}
}
return $result;
}
public function privateKeys() : array
{
// \glob("{$this->homedir}/*.key");
}
public static function isSupported() : bool
{
return \defined('PKCS7_DETACHED');
@ -110,7 +166,7 @@ class OpenSSL
$this->certificate,
$this->privateKey,
$this->headers,
$detached ? \PKCS7_DETACHED | \PKCS7_BINARY : \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR
$detached ? \PKCS7_DETACHED | \PKCS7_BINARY : 0, // | PKCS7_NOCERTS | PKCS7_NOATTR
$this->untrusted_certificates_filename
)) {
throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
@ -153,16 +209,20 @@ class OpenSSL
throw new \RuntimeException('OpenSSL sign: failed to find p7s');
}
public function verify(/*string|Temporary*/$input, ?string $signers_certificates_filename = null, bool $returnBody)
/**
* $opaque = true, when the message is not detached
*/
public function verify(/*string|Temporary*/$input, ?string $signers_certificates_filename = null, bool $opaque = false)
{
if (\is_string($input)) {
$tmp = new Temporary('smimein-');
if (!$tmp->putContents($input)) {
return null;
}
$opaque |= \str_contains($input, 'application/pkcs7-mime') || \str_contains($input, 'application/x-pkcs7-mime');
$input = $tmp;
}
$output = $returnBody ? new Temporary('smimeout-') : null;
$output = $opaque ? new Temporary('smimeout-') : null;
if (true !== \openssl_pkcs7_verify(
$input->filename(),
// $flags = 0, // \PKCS7_NOVERIFY | \PKCS7_NOCHAIN | \PKCS7_NOSIGS

View file

@ -23,8 +23,8 @@
<!-- /ko -->
<!-- ko if: canGnuPG -->
<details>
<summary class="legend">GnuPG</summary>
<details style="margin:1em 0">
<summary style="font-size:larger;font-weight:bold">GnuPG</summary>
<table class="table table-hover list-table">
<tbody><tr><th colspan="4" data-i18n="SETTINGS_OPENPGP/TITLE_PRIVATE">Private keys</th></tr></tbody>
<tbody data-bind="foreach: gnupgPrivateKeys, i18nUpdate: gnupgPrivateKeys">
@ -71,8 +71,8 @@
<!-- /ko -->
<!-- ko if: canOpenPGP -->
<details>
<summary class="legend">OpenPGP.js</summary>
<details style="margin:1em 0">
<summary style="font-size:larger;font-weight:bold">OpenPGP.js</summary>
<table class="table table-hover list-table">
<tbody><tr><th colspan="4" data-i18n="SETTINGS_OPENPGP/TITLE_PRIVATE">Private keys</th></tr></tbody>
<tbody data-bind="foreach: openpgpkeysPrivate, i18nUpdate: openpgpkeysPrivate">
@ -117,8 +117,36 @@
</details>
<!-- /ko -->
<details>
<summary class="legend">Mailvelope</summary>
<details style="margin:1em 0">
<summary style="font-size:larger;font-weight:bold">Mailvelope</summary>
<a data-bind="visible: !canMailvelope" href="https://mailvelope.com/en/help" target="_blank" data-i18n="SETTINGS_OPENPGP/GET_MAILVELOPE"></a>
<div id="mailvelope-settings" style="height:40em" data-bind="visible: canMailvelope"></div>
</details>
<details>
<summary class="legend">S/MIME Certificates</summary>
<table class="table table-hover list-table">
<tbody data-bind="foreach: smimeCertificates">
<tr>
<td>
<span data-bind="visible: smimesign" class="fontastic" data-i18n="[title]CRYPTO/VERIFY"></span>
<span data-bind="visible: smimeencrypt" class="fontastic" data-i18n="[title]CRYPTO/ENCRYPT">🔒</span>
<span class="key-name" data-bind="text: CN"></span>
<span class="key-user" data-bind="text: emailAddress"></span>
</td>
<td>
<time data-time-format="FULL" data-bind="attr:{datetime:validTo}"></time>
</td>
<!--
<td>
<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>
</td>
<td>
<span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td>
-->
</tr>
</tbody>
</table>
</details>