diff --git a/dev/App/User.js b/dev/App/User.js index 386a1c6e6..21d7b588b 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -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); diff --git a/dev/Settings/User/Security.js b/dev/Settings/User/Security.js index cf1a393b4..cabde4a80 100644 --- a/dev/Settings/User/Security.js +++ b/dev/Settings/User/Security.js @@ -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; diff --git a/dev/Stores/User/SMime.js b/dev/Stores/User/SMime.js new file mode 100644 index 000000000..f5f5dd312 --- /dev/null +++ b/dev/Stores/User/SMime.js @@ -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); + }); +}; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index f752f9317..c9bfe51df 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -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); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php index 16ef2d486..026edeed3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php @@ -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); } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/smime.php b/snappymail/v/0.0.0/app/libraries/snappymail/smime.php deleted file mode 100644 index 1c9ed5540..000000000 --- a/snappymail/v/0.0.0/app/libraries/snappymail/smime.php +++ /dev/null @@ -1,199 +0,0 @@ -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 */ - { - } - -} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php b/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php index c27892e3f..fb22ce754 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php @@ -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 diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsSecurity.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsSecurity.html index 90704ce4c..03423e30e 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsSecurity.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsSecurity.html @@ -23,8 +23,8 @@ -
-GnuPG +
+GnuPG @@ -71,8 +71,8 @@ -
-OpenPGP.js +
+OpenPGP.js
Private keys
@@ -117,8 +117,36 @@ -
-Mailvelope +
+Mailvelope
+ +
+S/MIME Certificates +
Private keys
+ + + + + + + +
+ + 🔒 + + + + +
+