mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-02-24 23:08:08 +08:00
Added a S/MIME certificates list to User settings #259
This commit is contained in:
parent
a22ae5fcf4
commit
3f08051e37
8 changed files with 172 additions and 219 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
17
dev/Stores/User/SMime.js
Normal 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);
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
{
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue