Merge branch 'master' into php81

This commit is contained in:
the-djmaze 2024-02-27 23:19:18 +01:00
commit 0985df857c
81 changed files with 1015 additions and 741 deletions

View file

@ -1,3 +1,50 @@
## 2.35.2 2024-02-26
### Added
- GnuPG error handling
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
### Changed
- Update Portuguese by @ner00
### Fixed
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
- Error 352 when getting Filters
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
- Nextcloud SetPassword(): Argument #1 must be of type SensitiveString, string given
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
## 2.35.1 2024-02-26
### Added
- Search functionality in Admin -> Config
- Cache S/MIME passphrases when "remember" is checked
- Import S/MIME certificate popup
- pre-verify S/MIME opaque signed messages so we have a body to view
- Sort PGP keys and S/MIME certificates on email address
- Optionally use existing private key to generate S/MIME certificate
### Changed
- Better handling to detect which PGP or S/MIME sign/encrypt to use
- Improved StorageType handling
- Cleanup and improved Capa handling
- OPEN_PGP should be OPENPGP as it is one word
- Use get_debug_type() instead of gettype()
- Require OpenSSL due to S/MIME
- AbstractProvider::IsActive() is now an abstract method and must be defined in child class
- Make better use of SnappyMail\SensitiveString
- Update Polish translation by @tinola
### Fixed
- Verify S/MIME signatures got broken allong the way while implementing this
- Generate S/MIME self-signed certificate failed to keep existing private key
- MIME parser RegExp didn't escape boundary which caused issues
- TypeError: b64Encode(...).match(...) is null on saving compose draft
- Fix timestampToString() for future dates
## 2.35.0 2024-02-20
### Added

View file

@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 81.641 |
|app.js |4.207.787 | 426.852 |
|admin.js |2.170.153 | 82.445 |
|app.js |4.207.787 | 429.565 |
|boot.js | 868.735 | 4.142 |
|libs.js | 658.812 | 192.625 |
|libs.js | 658.812 | 192.786 |
|sieve.js | 0 | 84.707 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 790.252 |
|TOTAL |8.240.095 | 793.930 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 40.142 | 73.606 | 13.445 | 60.877 | 12.047 |
|app.min.js | 515.367 | 193.738 |139.456 | 65.570 |110.485 | 56.281 |
|admin.min.js | 256.831 | 40.573 | 73.606 | 13.585 | 60.877 | 12.188 |
|app.min.js | 515.367 | 195.013 |139.456 | 66.135 |110.485 | 56.623 |
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|libs.min.js | 584.772 | 92.664 |180.901 | 34.414 |155.182 | 30.850 |
|libs.min.js | 584.772 | 92.746 |180.901 | 34.452 |155.182 | 30.890 |
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 288.486 |358.761 |101.186 |299.485 | 88.134 |
|TOTAL user+sieve |1.217.635 | 329.650 |358.761 |111.551 |299.485 | 97.493 |
|TOTAL admin | 959.099 | 134.890 |292.911 | 49.061 |249.877 | 43.900 |
|TOTAL user |1.217.635 | 289.843 |358.761 |101.789 |299.485 | 88.516 |
|TOTAL user+sieve |1.217.635 | 331.007 |358.761 |112.154 |299.485 | 97.875 |
|TOTAL admin | 959.099 | 135.403 |292.911 | 49.239 |249.877 | 44.081 |
For a user it is around 69% smaller and faster than traditional RainLoop.
@ -188,8 +188,8 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 84.435 | 46.946 | 17.612 | 15.091 |
|app.min.css | 274.947 | 67.825 | 39.647 | 15.526 | 13.540 |
|app.css | 340.331 | 84.472 | 46.946 | 17.622 | 15.112 |
|app.min.css | 274.947 | 67.857 | 39.647 | 15.537 | 13.549 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.576 | | 7.013 | 6.096 |

View file

@ -69,10 +69,10 @@ export const GnuPGUserStore = new class {
}
};
if (isPrivate) {
key.password = async (btnTxt = 'SIGN') => {
key.password = async (btnTxt = 'CRYPTO/SIGN') => {
const pass = await Passphrases.ask(key,
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
'CRYPTO/'+btnTxt
btnTxt
);
pass && pass.remember && Passphrases.set(key, pass.password);
return pass?.password;
@ -82,8 +82,8 @@ export const GnuPGUserStore = new class {
if (key.armor) {
callback && callback();
} else {
let pass = isPrivate ? await key.password('POPUP_VIEW_TITLE') : '';
if (null != pass) {
let pass = isPrivate ? await key.password('OPENPGP/POPUP_VIEW_TITLE') : '';
if (null != pass) try {
const result = await Remote.post('GnupgExportKey', null, {
keyId: key.id,
isPrivate: isPrivate,
@ -95,6 +95,8 @@ export const GnuPGUserStore = new class {
} else {
Passphrases.delete(key);
}
} catch (e) {
Passphrases.delete(key);
}
}
return key.armor;
@ -182,15 +184,20 @@ export const GnuPGUserStore = new class {
uid: message.uid,
partId: pgpInfo.partId,
keyId: key.id,
passphrase: await key.password('DECRYPT'),
passphrase: await key.password('CRYPTO/DECRYPT'),
data: '' // message.plain() optional
}
if (null !== params.passphrase) {
const result = await Remote.post('GnupgDecrypt', null, params);
if (result?.Result?.data) {
return result.Result;
if (null != params.passphrase) {
try {
const response = await Remote.post('GnupgDecrypt', null, params);
if (response?.Result?.data) {
return response.Result;
}
throw response;
} catch (e) {
Passphrases.delete(key);
throw e;
}
Passphrases.delete(key);
}
}
}

View file

@ -65,6 +65,7 @@
overflow: hidden;
overflow-y: auto;
min-width: 100px;
color: var(--folders-color, #333);
}
hr {
@ -77,22 +78,18 @@
white-space: nowrap;
a {
display: block;
position: relative;
z-index: 1;
line-height: 34px;
background-color: transparent;
vertical-align: middle;
color: var(--folders-disabled-color, #666);
border-left: 3px solid transparent;
color: var(--folders-color, #333);
display: block;
line-height: 34px;
padding: 0 2em 0 @folderItemPadding;
position: relative;
text-decoration: none;
vertical-align: middle;
z-index: 1;
&.selectable {
color: var(--folders-color, #333);
cursor: pointer;
&:hover {
@ -115,6 +112,7 @@
}
}
&:not(.selectable) {
color: var(--folders-disabled-color, #666);
cursor: default;
font-style: italic;
}

View file

@ -52,10 +52,13 @@ export class IdentityPopupView extends AbstractViewPopup {
if (oData.Result.x509) {
identity.smimeKey(oData.Result.pkey);
identity.smimeCertificate(oData.Result.x509);
} else {
this.submitError(oData.ErrorMessage);
}
}, {
name: identity.name(),
email: identity.email(),
privateKey: identity.smimeKey(),
passphrase: pass.password
});
}

View file

@ -587,10 +587,13 @@ export class MailMessageView extends AbstractViewRight {
}
} else {
// TODO: translate
alert('Decryption failed, canceled or not possible');
throw 'Decryption failed, canceled or not possible';
}
})
.catch(e => console.error(e));
.catch(e => {
console.error(e)
alert(e.message);
});
}
pgpVerify(/*self, event*/) {

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.35.0</upstream>.
This app packages SnappyMail <upstream>2.35.2</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client.

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
VERSION=2.35.0
VERSION=2.35.2
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \

View file

@ -3,7 +3,7 @@
<id>snappymail</id>
<name>SnappyMail</name>
<summary>SnappyMail Webmail</summary>
<version>2.35.0</version>
<version>2.35.2</version>
<licence>agpl</licence>
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**

View file

@ -72,7 +72,7 @@ class InstallStep implements IRepairStep
if ('12345' == $sPassword || !$sPassword) {
$output->info('Generate admin password');
$sPassword = \substr(\base64_encode(\random_bytes(16)), 0, 12);
$oConfig->SetPassword($sPassword);
$oConfig->SetPassword(new \SnappyMail\SensitiveString($sPassword));
\RainLoop\Utils::saveFile(APP_PRIVATE_DATA . 'admin_password.txt', $sPassword . "\n");
$bSave = true;
}

View file

@ -91,7 +91,6 @@ class SnappyMailHelper
*/
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
try {
$oActions->Logger()->AddSecret($aCredentials[2]);
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if ($oAccount) {
// Must be here due to bug #1241
@ -202,9 +201,10 @@ class SnappyMailHelper
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
}
public static function decodePassword(string $sPassword, string $sSalt)/* : mixed */
public static function decodePassword(string $sPassword, string $sSalt) : ?\SnappyMail\SensitiveString
{
static::loadApp();
return \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
return $result ? new \SnappyMail\SensitiveString($result) : $result;
}
}

View file

@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions()
sub script_snappymail_versions
{
return ( "2.35.0" );
return ( "2.35.2" );
}
sub script_snappymail_version_desc

View file

@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
"version": "2.35.0",
"version": "2.35.2",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",

View file

@ -8,6 +8,7 @@ class LdapConfig
public const CONFIG_SERVER = "server";
public const CONFIG_PROTOCOL_VERSION = "server_version";
public const CONFIG_STARTTLS = "starttls";
public const CONFIG_MAIL_PREFIX = "mail_prefix";
public const CONFIG_BIND_USER = "bind_user";
public const CONFIG_BIND_PASSWORD = "bind_password";
@ -30,6 +31,7 @@ class LdapConfig
public $server;
public $protocol;
public $starttls;
public $mail_prefix;
public $bind_user;
public $bind_password;
public $user_base;
@ -51,6 +53,7 @@ class LdapConfig
$ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER));
$ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3));
$ldap->starttls = (bool)trim($config->Get("plugin", self::CONFIG_STARTTLS));
$ldap->mail_prefix = trim($config->Get("plugin", self::CONFIG_MAIL_PREFIX));
$ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER));
$ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD));
$ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE));

View file

@ -69,7 +69,8 @@ class LdapIdentities implements IIdentities
$this->config->user_base,
$this->config->user_objectclass,
$this->config->user_field_name,
$this->config->user_field_mail
$this->config->user_field_mail,
$this->config->mail_prefix
);
} catch (LdapException $e) {
return []; // exceptions are only thrown from the handleerror function that does logging already
@ -104,7 +105,8 @@ class LdapIdentities implements IIdentities
$this->config->group_base,
$this->config->group_objectclass,
$this->config->group_field_name,
$this->config->group_field_mail
$this->config->group_field_mail,
$this->config->mail_prefix
);
} catch (LdapException $e) {
return []; // exceptions are only thrown from the handleerror function that does logging already
@ -241,7 +243,7 @@ class LdapIdentities implements IIdentities
* @return LdapResult[]
* @throws LdapException
*/
private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField): array
private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField, string $mailPrefix): array
{
$this->EnsureBound();
@ -261,6 +263,8 @@ class LdapIdentities implements IIdentities
return [];
}
$entries = $this->CleanupMailAddresses($entries, $mailField, $mailPrefix);
$results = [];
for ($i = 0; $i < $entries["count"]; $i++) {
$entry = $entries[$i];
@ -276,6 +280,49 @@ class LdapIdentities implements IIdentities
return $results;
}
// Function CleanupMailAddresses(): If a prefix is given this function removes addresses without / with the wrong prefix and then the prefix itself from all remaining values.
// This is usefull for example for importing Active Directory LDAP entry "proxyAddresses" which can hold different address types with prefixes like "X400:", "smtp:" "sip:" and others.
/**
@param array $entries
@param string $mailField
@paraam string $mailPrefix
@return array
*/
private function CleanupMailAddresses(array $entries, string $mailField, string $mailPrefix)
{
if (!empty($mailPrefix)) {
for ($i = 0; $i < $entries["count"]; $i++) {
// Remove addresses without the given prefix
$entries[$i]["$mailField"] = array_filter($entries[$i]["$mailField"],
function($prefixMail)
{
// $mailPrefix can't be used here, because it's nailed to the CleanupMailAddresses function and can't be passed to the array_filter function afaik.
// Ideas to avoid this are welcome.
if (stripos($prefixMail, $this->config->mail_prefix) === 0) {
return TRUE;
}
return FALSE;
}
);
// Set "count" to new value
$newcount = count($entries[$i]["$mailField"]);
if (array_key_exists("count", $entries[$i]["$mailField"])) {
$newcount = $newcount - 1;
}
$entries[$i]["$mailField"]["count"] = $newcount;
// Remove the prefix
for ($j = 0; $j < $entries[$i]["$mailField"]["count"]; $j++) {
$mailPrefixLen = mb_strlen($mailPrefix);
$entries[$i]["$mailField"][$j] = substr($entries[$i]["$mailField"][$j], $mailPrefixLen);
}
}
}
return $entries;
}
/**
* @param array $entry
* @param string $attribute

View file

@ -8,10 +8,10 @@ class LdapIdentitiesPlugin extends AbstractPlugin
{
const
NAME = 'LDAP Identities',
VERSION = '2.2',
VERSION = '2.3',
AUTHOR = 'FWest98',
URL = 'https://github.com/FWest98',
RELEASE = '2024-02-22',
RELEASE = '2024-02-27',
REQUIRED = '2.20.0',
CATEGORY = 'Accounts',
DESCRIPTION = 'Adds functionality to import account identities from LDAP.';
@ -57,13 +57,19 @@ class LdapIdentitiesPlugin extends AbstractPlugin
->SetLabel("LDAP Protocol Version")
->SetType(PluginPropertyType::SELECTION)
->SetDefaultValue([2, 3]),
Property::NewInstance(LdapConfig::CONFIG_STARTTLS)
->SetLabel("Use StartTLS")
->SetType(PluginPropertyType::BOOL)
->SetDescription("Whether or not to use TLS encrypted connection")
->SetDefaultValue(true),
Property::NewInstance(LdapConfig::CONFIG_MAIL_PREFIX)
->SetLabel("Email prefix")
->SetType(PluginPropertyType::STRING)
->SetDescription("Only addresses with this prefix will be used as identity. The prefix is removed from the identity list.\nThis is useful for example to import identities from Exchange, which stores mail addresses in the ProxyAddresses attribut of Active Directory with \"smtp:\" as prefix. (e.g. \"smtp:john.doe@topsecret.info\")\n-> To use addresses set by Exchange use \"smtp:\" as prefix.")
->SetDefaultValue(""),
Property::NewInstance(LdapConfig::CONFIG_BIND_USER)
->SetLabel("Bind User DN")
->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind")

View file

@ -329,7 +329,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
);
}
/*
if ('storage' === $sName || 'storage-local' === $sName) {
if ($this->Config()->Get('plugin', 'storage', false) && ('storage' === $sName || 'storage-local' === $sName)) {
require_once __DIR__ . '/storage.php';
$oDriver = new \NextcloudStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local');
}
@ -346,6 +346,11 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
\RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true),
/*
\RainLoop\Plugins\Property::NewInstance('storage')->SetLabel('Use Nextcloud user ID in config storage path')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
*/
\RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)

View file

@ -184,7 +184,12 @@ class SieveClient extends \MailSo\Net\NetClient
public function Logout() : void
{
if ($this->bIsLoggined) {
$this->sendRequestWithCheck('LOGOUT');
try {
$this->sendRequestWithCheck('LOGOUT');
} catch (\Throwable $e) {
// https://github.com/the-djmaze/snappymail/issues/1455
$this->writeLogException($e, \LOG_WARNING, false);
}
$this->bIsLoggined = false;
}
}
@ -408,6 +413,7 @@ class SieveClient extends \MailSo\Net\NetClient
if (null === $sResponseBuffer) {
break;
}
// \MailSo\Imap\Enumerations\ResponseStatus
$bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO'));
// convertEndOfLine
$sLine = \trim($sResponseBuffer);

View file

@ -81,7 +81,7 @@ trait Accounts
$aAccounts = $this->GetAccounts($oMainAccount);
$sEmail = \trim($this->GetActionParam('email', ''));
$sPassword = $this->GetActionParam('password', '');
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('password', ''));
$sName = \trim($this->GetActionParam('name', ''));
$bNew = !empty($this->GetActionParam('new', 1));
@ -92,8 +92,8 @@ trait Accounts
throw new ClientException(Notifications::AccountDoesNotExist);
}
if ($bNew || $sPassword) {
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false);
if ($bNew || \strlen($oPassword)) {
$oNewAccount = $this->LoginProcess($sEmail, $oPassword, false);
$aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount);
} else {
$aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]);
@ -261,7 +261,11 @@ trait Accounts
if (!$oIdentity->FromJSON($this->GetActionParams(), true)) {
throw new ClientException(Notifications::InvalidInputArgument);
}
/* // TODO: verify private key for certificate?
if ($oIdentity->smimeCertificate && $oIdentity->smimeKey) {
new \SnappyMail\SMime\Certificate($oIdentity->smimeCertificate, $oIdentity->smimeKey);
}
*/
$this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity);
return $this->TrueResponse();
}

View file

@ -60,9 +60,9 @@ trait AdminDomains
public function DoAdminDomainMatch() : array
{
$sEmail = $this->GetActionParam('username');
$sPassword = '********';
$oPassword = new \SnappyMail\SensitiveString('********');
$sLogin = '';
$this->resolveLoginCredentials($sEmail, $sPassword, $sLogin);
$this->resolveLoginCredentials($sEmail, $oPassword, $sLogin);
$oDomain = \str_contains($sEmail, '@')
? $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true)
: null;

View file

@ -1065,8 +1065,7 @@ trait Messages
}
}
$sPassphrase = $this->GetActionParam('signPassphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('signPassphrase', ''));
$sFingerprint = $this->GetActionParam('signFingerprint', '');
if ($sFingerprint) {
@ -1084,7 +1083,7 @@ trait Messages
$oMessage->SubParts->Clear();
$oMessage->Attachments()->Clear();
$GPG->addSignKey($sFingerprint, $sPassphrase);
$GPG->addSignKey($sFingerprint, $oPassphrase);
$GPG->setsignmode(GNUPG_SIG_MODE_DETACH);
$sSignature = $GPG->signStream($fp);
if (!$sSignature) {
@ -1127,7 +1126,7 @@ trait Messages
$SMIME = $this->SMIME();
$SMIME->setCertificate($sCertificate);
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
$SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
$sSignature = $SMIME->sign($tmp, $detached);
if (!$sSignature) {

View file

@ -24,7 +24,7 @@ trait Pgp
$GPG = $this->GnuPG();
if ($GPG) {
$keys = $GPG->keyInfo('');
$keys = $GPG->allKeysInfo('');
foreach ($keys['public'] as $key) {
$key = $GPG->export($key['subkeys'][0]['fingerprint'] ?: $key['subkeys'][0]['keyid']);
if ($key) {
@ -47,7 +47,7 @@ trait Pgp
/**
* @throws \MailSo\RuntimeException
*/
public function GnuPG() : ?GnuPG
public function GnuPG() : ?\SnappyMail\PGP\PGPInterface
{
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
@ -81,7 +81,7 @@ trait Pgp
if (\is_link($link) || \symlink($homedir, $link)) {
$homedir = $link;
} else {
\error_log("symlink('{$homedir}', '{$link}') failed");
$this->logWrite("symlink('{$homedir}', '{$link}') failed", \LOG_WARNING, 'GnuPG');
}
}
// Else try ~/.gnupg/ + hash(email address)
@ -97,10 +97,6 @@ trait Pgp
$homedir = $tmpdir;
}
}
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
throw new \Exception("socket name for '{$homedir}/S.gpg-agent.extra' is too long");
}
}
return GnuPG::getInstance($homedir);
@ -113,15 +109,14 @@ trait Pgp
return $this->FalseResponse();
}
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$GPG->addDecryptKey($this->GetActionParam('keyId', ''), $sPassphrase);
$GPG->addDecryptKey($this->GetActionParam('keyId', ''), $oPassphrase);
$sData = $this->GetActionParam('data', '');
$oPart = null;
$result = [
'data' => '',
'data' => null,
'signatures' => []
];
if ($sData) {
@ -131,11 +126,13 @@ trait Pgp
$this->initMailClientConnection();
$this->MailClient()->MessageMimeStream(
function ($rResource) use ($GPG, &$result, &$oPart) {
if (\is_resource($rResource)) {
if (\is_resource($rResource)) try {
$result['data'] = $GPG->decryptStream($rResource);
// $oPart = \MailSo\Mime\Part::FromString($result);
// $GPG->decryptStream($rResource, $rStreamHandle);
// $oPart = \MailSo\Mime\Part::FromStream($rStreamHandle);
} catch (\Throwable $e) {
$result = $e;
}
},
$this->GetActionParam('folder', ''),
@ -149,23 +146,28 @@ trait Pgp
// $result['signatures'] = $oPart->SubParts[0];
}
if ($result instanceof \Throwable) {
throw $result;
}
return $this->DefaultResponse($result);
}
public function DoGnupgGetKeys() : array
{
$GPG = $this->GnuPG();
return $this->DefaultResponse($GPG ? $GPG->keyInfo('') : false);
return $this->DefaultResponse($GPG ? $GPG->allKeysInfo('') : false);
}
public function DoGnupgExportKey() : array
{
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = $this->GetActionParam('isPrivate', '')
? new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''))
: null;
$GPG = $this->GnuPG();
return $this->DefaultResponse($GPG ? $GPG->export(
$this->GetActionParam('keyId', ''),
$sPassphrase
$oPassphrase
) : false);
}
@ -176,11 +178,10 @@ trait Pgp
if ($GPG) {
$sName = $this->GetActionParam('name', '');
$sEmail = $this->GetActionParam('email', '');
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$fingerprint = $GPG->generateKey(
$sName ? "{$sName} <{$sEmail}>" : $sEmail,
$sPassphrase
$oPassphrase
);
}
return $this->DefaultResponse($fingerprint);

View file

@ -45,9 +45,10 @@ trait Response
public function ExceptionResponse(\Throwable $oException) : array
{
$iErrorCode = 0;
$sErrorMessage = '';
$iErrorCode = Notifications::UnknownError;
$sErrorMessage = $oException->getMessage();
$sErrorMessageAdditional = '';
$iExceptionCode = 0;
if ($oException instanceof \RainLoop\Exceptions\ClientException) {
$iErrorCode = $oException->getCode();
@ -56,8 +57,7 @@ trait Response
}
$sErrorMessageAdditional = $oException->getAdditionalMessage();
} else {
$iErrorCode = Notifications::UnknownError;
$sErrorMessage = $oException->getCode().' - '.$oException->getMessage();
$iExceptionCode = $oException->getCode();
}
$this->logException($oException->getPrevious() ?: $oException);
@ -65,7 +65,8 @@ trait Response
return $this->DefaultResponse(false, [
'ErrorCode' => $iErrorCode,
'ErrorMessage' => $sErrorMessage,
'ErrorMessageAdditional' => $sErrorMessageAdditional
'ErrorMessageAdditional' => $sErrorMessageAdditional,
'ExceptionCode' => $iExceptionCode
]);
}

View file

@ -53,21 +53,18 @@ trait SMime
}
/**
* Can be use by Identity
* Can be used by Identity
*/
public function DoSMimeCreateCertificate() : array
{
$oAccount = $this->getAccountFromToken();
$sName = $this->GetActionParam('name', '') ?: $oAccount->Name();
$sEmail = $this->GetActionParam('email', '') ?: $oAccount->Email();
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$cert = new Certificate();
$cert->distinguishedName['commonName'] = $sName;
$cert->distinguishedName['emailAddress'] = $sEmail;
$result = $cert->createSelfSigned($sPassphrase);
$cert->distinguishedName['commonName'] = $this->GetActionParam('name', '') ?: $oAccount->Name();
$cert->distinguishedName['emailAddress'] = $this->GetActionParam('email', '') ?: $oAccount->Email();
$result = $cert->createSelfSigned($oPassphrase, $this->GetActionParam('privateKey', ''));
return $this->DefaultResponse($result ?: false);
}
@ -78,8 +75,7 @@ trait SMime
$sPartId = $this->GetActionParam('partId', '');
$sCertificate = $this->GetActionParam('certificate', '');
$sPrivateKey = $this->GetActionParam('privateKey', '');
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$this->initMailClientConnection();
$oImapClient = $this->ImapClient();
@ -106,7 +102,7 @@ trait SMime
$SMIME = $this->SMIME();
$SMIME->setCertificate($sCertificate);
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
$SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
$result = $SMIME->decrypt($sBody);
return $this->DefaultResponse($result ?: false);
@ -142,9 +138,9 @@ trait SMime
$sBody = \trim($sBody);
$certificates = [];
\openssl_pkcs7_read(
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----",
"-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
$certificates
) || \error_log("OpenSSL openssl_pkcs7_read: " . \openssl_error_string());
) || $this->logWrite("openssl_pkcs7_read: " . \openssl_error_string(), \LOG_ERR, 'OpenSSL');
foreach ($certificates as $certificate) {
$this->SMIME()->storeCertificate($certificate);
}
@ -179,7 +175,7 @@ trait SMime
$sBody = \trim($sBody);
$certificates = [];
\openssl_pkcs7_read(
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----",
"-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
$certificates
);

View file

@ -36,10 +36,10 @@ trait User
public function DoLogin() : array
{
$sEmail = \MailSo\Base\Utils::Trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', '');
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
try {
$oAccount = $this->LoginProcess($sEmail, $sPassword);
$oAccount = $this->LoginProcess($sEmail, $oPassword);
} catch (\Throwable $oException) {
$this->loginErrorDelay();
throw $oException;

View file

@ -22,10 +22,7 @@ trait UserAuth
/**
* @throws \RainLoop\Exceptions\ClientException
*/
public function resolveLoginCredentials(string &$sEmail,
#[\SensitiveParameter]
string &$sPassword,
string &$sLogin): void
public function resolveLoginCredentials(string &$sEmail, \SnappyMail\SensitiveString $oPassword, string &$sLogin): void
{
$sEmail = \MailSo\Base\Utils::Trim($sEmail);
if ($this->Config()->Get('login', 'login_lowercase', true)) {
@ -101,43 +98,40 @@ trait UserAuth
}
}
$sPassword = (string) $oPassword;
$this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword));
$this->logMask($sPassword);
$sLogin = $sEmail;
if ($this->Config()->Get('login', 'login_lowercase', true)) {
$sLogin = \mb_strtolower($sLogin);
}
$this->logMask($sPassword);
$this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword));
$this->logMask($sPassword);
$oPassword->setValue($sPassword);
}
/**
* @throws \RainLoop\Exceptions\ClientException
*/
public function LoginProcess(string &$sEmail,
#[\SensitiveParameter]
string &$sPassword,
bool $bMainAccount = true
): Account
public function LoginProcess(string &$sEmail, \SnappyMail\SensitiveString $oPassword, bool $bMainAccount = true): Account
{
$sInputEmail = $sEmail;
$this->logMask($sPassword);
$sLogin = '';
$this->resolveLoginCredentials($sEmail, $sPassword, $sLogin);
$this->resolveLoginCredentials($sEmail, $oPassword, $sLogin);
if (!\str_contains($sEmail, '@') || !\strlen($sPassword)) {
if (!\str_contains($sEmail, '@') || !\strlen($oPassword)) {
throw new ClientException(Notifications::InvalidInputArgument);
}
$oAccount = null;
try {
$oAccount = $bMainAccount
? MainAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $sPassword, true)
: AdditionalAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $sPassword, true);
? MainAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $oPassword, true)
: AdditionalAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $oPassword, true);
if (!$oAccount) {
throw new ClientException(Notifications::AuthError);
}

View file

@ -111,17 +111,15 @@ class ActionsAdmin extends Actions
public function DoAdminLogin() : array
{
$sLogin = trim($this->GetActionParam('Login', ''));
$sPassword = $this->GetActionParam('Password', '');
$this->logMask($sPassword);
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
$totp = $this->Config()->Get('security', 'admin_totp', '');
// \explode(':',`getent shadow root`)[1];
if (!\strlen($sLogin) || !\strlen($sPassword) ||
if (!\strlen($sLogin) || !\strlen($oPassword) ||
!$this->Config()->Get('security', 'allow_admin_panel', true) ||
$sLogin !== $this->Config()->Get('security', 'admin_login', '') ||
!$this->Config()->ValidatePassword($sPassword)
!$this->Config()->ValidatePassword($oPassword)
|| ($totp && !\SnappyMail\TOTP::Verify($totp, $this->GetActionParam('TOTP', ''))))
{
$this->LoggerAuthHelper(null, $this->getAdditionalLogParamsByUserLogin($sLogin, true), true);
@ -183,17 +181,13 @@ class ActionsAdmin extends Actions
$bResult = false;
$oConfig = $this->Config();
$sPassword = $this->GetActionParam('Password', '');
$this->logMask($sPassword);
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
$sNewPassword = $this->GetActionParam('newPassword', '');
if (\strlen($sNewPassword)) {
$this->logMask($sNewPassword);
}
$oNewPassword = new \SnappyMail\SensitiveString($this->GetActionParam('newPassword', ''));
$passfile = APP_PRIVATE_DATA.'admin_password.txt';
if ($oConfig->ValidatePassword($sPassword)) {
if ($oConfig->ValidatePassword($oPassword)) {
$sLogin = \trim($this->GetActionParam('Login', ''));
if (\strlen($sLogin)) {
$oConfig->Set('security', 'admin_login', $sLogin);
@ -201,9 +195,9 @@ class ActionsAdmin extends Actions
$oConfig->Set('security', 'admin_totp', $this->GetActionParam('TOTP', ''));
if (\strlen($sNewPassword)) {
$oConfig->SetPassword($sNewPassword);
if (\is_file($passfile) && \trim(\file_get_contents($passfile)) !== $sNewPassword) {
if (\strlen($oNewPassword)) {
$oConfig->SetPassword($oNewPassword);
if (\is_file($passfile) && \trim(\file_get_contents($passfile)) !== (string) $oNewPassword) {
\unlink($passfile);
}
}

View file

@ -120,25 +120,14 @@ class Application extends \RainLoop\Config\AbstractConfig
parent::Set($sSectionKey, $sParamKey, $mParamValue);
}
public function SetPassword(
#[\SensitiveParameter]
string $sPassword
) : void
public function SetPassword(\SnappyMail\SensitiveString $oPassword) : void
{
$this->Set('security', 'admin_password', \password_hash($sPassword, PASSWORD_DEFAULT));
$this->Set('security', 'admin_password', \password_hash($oPassword, PASSWORD_DEFAULT));
}
public function ValidatePassword(
#[\SensitiveParameter]
string $sPassword
) : bool
public function ValidatePassword(\SnappyMail\SensitiveString $oPassword) : bool
{
$sConfigPassword = (string) $this->Get('security', 'admin_password', '');
if (32 == \strlen($sPassword) && \md5(APP_SALT.$sPassword.APP_SALT) === $sConfigPassword) {
$this->SetPassword($sPassword);
return true;
}
return \strlen($sPassword) && \password_verify($sPassword, $sConfigPassword);
return \strlen($oPassword) && \password_verify($oPassword, $this->Get('security', 'admin_password', ''));
}
public function Save() : bool

View file

@ -15,7 +15,7 @@ abstract class Account implements \JsonSerializable
private string $sLogin = '';
private ?SensitiveString $sPassword = null;
private ?SensitiveString $oPassword = null;
private string $sSmtpLogin = '';
@ -46,7 +46,7 @@ abstract class Account implements \JsonSerializable
public function IncPassword() : string
{
return $this->sPassword ? $this->sPassword->getValue() : '';
return $this->oPassword ? $this->oPassword->getValue() : '';
}
public function OutLogin() : string
@ -66,16 +66,13 @@ abstract class Account implements \JsonSerializable
$this->sEmail,
$this->sLogin,
// \json_encode($this->Domain()),
// $this->sPassword
// $this->oPassword
]));
}
public function SetPassword(
#[\SensitiveParameter]
string $sPassword
) : void
public function SetPassword(SensitiveString $oPassword) : void
{
$this->sPassword = new SensitiveString($sPassword);
$this->oPassword = $oPassword;
}
public function SetSmtpPassword(
@ -125,12 +122,11 @@ abstract class Account implements \JsonSerializable
public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions,
string $sEmail, string $sLogin,
#[\SensitiveParameter]
string $sPassword,
SensitiveString $oPassword,
bool $bThrowException = false): ?self
{
$oAccount = null;
if ($sEmail && $sLogin && $sPassword) {
if ($sEmail && $sLogin && \strlen($oPassword)) {
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
if ($oDomain) {
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
@ -138,7 +134,7 @@ abstract class Account implements \JsonSerializable
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
$oAccount->SetPassword($sPassword);
$oAccount->SetPassword($oPassword);
$oAccount->oDomain = $oDomain;
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
@ -194,25 +190,22 @@ abstract class Account implements \JsonSerializable
$oActions,
$aAccountHash['email'],
$aAccountHash['login'],
$aAccountHash['pass'],
new SensitiveString($aAccountHash['pass']),
$bThrowExceptionOnFalse
);
if ($oAccount) {
if (isset($aAccountHash['name'])) {
$oAccount->sName = $aAccountHash['name'];
}
$oActions->logMask($oAccount->IncPassword());
// init smtp user/password
if (isset($aAccountHash['smtp'])) {
$oAccount->sSmtpLogin = $aAccountHash['smtp']['user'];
$oAccount->SetSmtpPassword($aAccountHash['smtp']['pass']);
$oActions->logMask($oAccount->sSmtpPassword);
}
// init proxy user/password
if (isset($aAccountHash['proxy'])) {
$oAccount->sProxyAuthUser = $aAccountHash['proxy']['user'];
$oAccount->SetProxyAuthPassword($aAccountHash['proxy']['pass']);
$oActions->logMask($oAccount->sProxyAuthPassword);
}
}
}
@ -240,7 +233,7 @@ abstract class Account implements \JsonSerializable
$oImapClient->Connect($oSettings);
$oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $oSettings));
$oSettings->Password = $this->sPassword;
$oSettings->Password = $this->oPassword;
return $this->netClientLogin($oImapClient, $oPlugins);
}
@ -264,7 +257,7 @@ abstract class Account implements \JsonSerializable
throw new RequireCredentialsException
}
*/
$oSettings->Password = $this->sSmtpPassword ?: $this->sPassword;
$oSettings->Password = $this->sSmtpPassword ?: $this->oPassword;
return $this->netClientLogin($oSmtpClient, $oPlugins);
}
@ -279,7 +272,7 @@ abstract class Account implements \JsonSerializable
$oSieveClient->Connect($oSettings);
$oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $oSettings));
$oSettings->Password = $this->sPassword;
$oSettings->Password = $this->oPassword;
return $this->netClientLogin($oSieveClient, $oPlugins);
}

View file

@ -37,6 +37,16 @@ class Identity implements \JsonSerializable
$this->sEmail = $sEmail;
}
function __get(string $name)
{
if (!\property_exists($this, $name)) {
$name = \substr($name, 1);
}
if (\property_exists($this, $name)) {
return $this->$name;
}
}
function toMime() : \MailSo\Mime\Email
{
return new \MailSo\Mime\Email($this->sEmail, $this->sName);

View file

@ -112,24 +112,16 @@ class ServiceActions
$this->oActions->logWrite('Action: '.$sMethodName, \LOG_INFO, 'JSON');
$aPost = $_POST ?? null;
if ($aPost) {
$this->oActions->SetActionParams($aPost, $sMethodName);
if ($_POST) {
$this->oActions->SetActionParams($_POST, $sMethodName);
$aPost = $_POST;
foreach ($aPost as $key => $value) {
if (false !== \stripos($key, 'Password')) {
// password & passphrase
if (false !== \stripos($key, 'pass')) {
$aPost[$key] = '*******';
// $this->oActions->logMask($value);
}
}
/*
switch ($sMethodName)
{
case 'DoLogin':
case 'DoAdminLogin':
case 'DoAccountAdd':
$this->oActions->logMask($this->oActions->GetActionParam('Password', ''));
break;
}
*/
$this->oActions->logWrite(Utils::jsonEncode($aPost), \LOG_INFO, 'POST');
} else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) {
$this->oActions->SetActionParams(array(
@ -561,14 +553,14 @@ class ServiceActions
(0 === $aData['Time'] || \time() - 10 < $aData['Time']))
{
$sEmail = \trim($aData['Email']);
$sPassword = $aData['Password'];
$oPassword = new \SnappyMail\SensitiveString($aData['Password']);
$aAdditionalOptions = (isset($aData['AdditionalOptions']) && \is_array($aData['AdditionalOptions']))
? $aData['AdditionalOptions'] : [];
try
{
$oAccount = $this->oActions->LoginProcess($sEmail, $sPassword);
$oAccount = $this->oActions->LoginProcess($sEmail, $oPassword);
if ($aAdditionalOptions) {
$bNeedToSettings = false;

View file

@ -5,6 +5,8 @@
namespace SnappyMail\GPG;
use SnappyMail\SensitiveString;
abstract class Base
{
const
@ -64,7 +66,13 @@ abstract class Base
function __construct(string $homedir)
{
$this->options['homedir'] = \rtrim($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->options['homedir'] = $homedir;
// \putenv("GNUPGHOME={$homedir}");
}
function __destruct()
@ -169,22 +177,13 @@ abstract class Base
}
/**
* Exports a public key
* Exports a public or private key
*/
public function export(string $fingerprint) /*: string|false*/
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
{
return false;
}
/**
* Exports a private key
*/
public function exportPrivateKey(string $fingerprint) /*: string|false*/
{
return false;
}
/**
* Imports a key
*/
@ -209,12 +208,11 @@ abstract class Base
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function keyInfo(string $pattern, int $private = 0) : array
public function keyInfo(string $pattern, bool $private = false) : array
{
return [];
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
@ -243,7 +241,7 @@ abstract class Base
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
{
return false;
}
@ -284,10 +282,7 @@ abstract class Base
/**
* Add a key for decryption
*/
public function addDecryptKey(string $fingerprint,
#[\SensitiveParameter]
string $passphrase
) : bool
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool
{
$this->decryptKeys[$fingerprint] = $passphrase;
// $this->decryptKeys[\substr($fingerprint, -16)] = $passphrase;
@ -306,10 +301,7 @@ abstract class Base
/**
* Add a key for signing
*/
public function addSignKey(string $fingerprint,
#[\SensitiveParameter]
string $passphrase
) : bool
public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool
{
$this->signKeys[$fingerprint] = $passphrase;
// $this->signKeys[\substr($fingerprint, -16)] = $passphrase;
@ -356,10 +348,7 @@ abstract class Base
];
}
public function addPassphrase($keyId,
#[\SensitiveParameter]
$passphrase
)
public function addPassphrase($keyId, SensitiveString $passphrase)
{
$this->passphrases[$keyId] = $passphrase;
return $this;
@ -367,10 +356,11 @@ abstract class Base
/**
* Toggle the armored output
* When true the output is ASCII
*/
public function setArmor(int $armor = 1) : bool
public function setArmor(bool $armor = true) : bool
{
$this->armor = !!$armor;
$this->armor = $armor;
return true;
}

View file

@ -7,7 +7,9 @@
namespace SnappyMail\GPG;
class PGP extends Base
use SnappyMail\SensitiveString;
class PGP extends Base implements \SnappyMail\PGP\PGPInterface
{
private
$_message,
@ -95,7 +97,7 @@ class PGP extends Base
$fclose = $this->setOutput($output);
if ($this->decryptKeys) {
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->decryptKeys);
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->decryptKeys));
}
$result = $this->exec(['--decrypt','--skip-verify']);
@ -137,6 +139,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
// \rewind($fp);
return $this->_decrypt($fp, $output);
}
@ -215,6 +218,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
\rewind($fp);
return $this->_encrypt($fp, $output);
}
@ -234,14 +238,14 @@ class PGP extends Base
return false;
}
protected function _exportKey($keyId, $private = false)
protected function _exportKey($keyId, bool $private = false)
{
$keys = $this->keyInfo($keyId, $private ? 1 : 0);
$keys = $this->keyInfo($keyId, $private);
if (!$keys) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
}
if ($private && $this->passphrases) {
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases);
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->passphrases));
}
$result = $this->exec([
$private ? '--export-secret-keys' : '--export',
@ -252,21 +256,19 @@ class PGP extends Base
}
/**
* Exports a public key
* Exports a public or private key
*/
public function export(string $fingerprint) /*: string|false*/
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
{
// \SnappyMail\Log::debug('GnuPG', "export({$fingerprint}, {$passphrase})");
if (null !== $passphrase) {
return $this
->addPassphrase($fingerprint, $passphrase)
->_exportKey($fingerprint, true);
}
return $this->_exportKey($fingerprint);
}
/**
* Exports a private key
*/
public function exportPrivateKey(string $fingerprint) /*: string|false*/
{
return $this->_exportKey($fingerprint, true);
}
/**
* Returns the errortext, if a function fails
*/
@ -296,8 +298,13 @@ class PGP extends Base
* Also saves revocation certificate in {homedir}/openpgp-revocs.d/
* https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html
*/
public function generateKey(PGPKeySettings $settings) /*: string|false*/
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/
{
$settings = new PGPKeySettings;
$settings->name = $uid;
$settings->email = $uid;
$settings->passphrase = $passphrase;
$arguments = [
'--batch',
'--yes',
@ -362,7 +369,7 @@ class PGP extends Base
$arguments = ['--import'];
if ($this->passphrases) {
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases);
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->passphrases));
} else {
$arguments[] = '--batch';
}
@ -421,13 +428,13 @@ class PGP extends Base
}
}
public function deleteKey(string $keyId, bool $private)
public function deleteKey(string $keyId, bool $private) : bool
{
$key = $this->keyInfo($keyId, $private ? 1 : 0);
$key = $this->keyInfo($keyId, $private);
if (!$key) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
}
// if (!$private && $this->keyInfo($keyId, 1)) {
// if (!$private && $this->keyInfo($keyId, true)) {
// throw new \Exception('Delete private key first: ' . $keyId);
// }
@ -448,7 +455,7 @@ class PGP extends Base
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function keyInfo(string $pattern, int $private = 0) : array
public function keyInfo(string $pattern, bool $private = false) : array
{
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys.
@ -572,6 +579,30 @@ class PGP extends Base
return $keys;
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function allKeysInfo(string $pattern) : array
{
$keys = [
'public' => [],
'private' => []
];
// Public
foreach (($this->keyinfo($pattern) ?: []) as $key) {
$key['can_verify'] = $key['can_sign'];
unset($key['can_sign']);
$keys['public'][] = $key;
}
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
foreach (($this->keyinfo($pattern, 1) ?: []) as $key) {
$key['can_decrypt'] = $key['can_encrypt'];
unset($key['can_encrypt']);
$keys['private'][] = $key;
}
return $keys;
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
@ -629,7 +660,7 @@ class PGP extends Base
foreach ($this->signKeys as $fingerprint => $pass) {
$arguments[] = '--local-user ' . \escapeshellarg($fingerprint);
}
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->signKeys);
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->signKeys));
}
$result = $this->exec($arguments);
@ -666,15 +697,16 @@ class PGP extends Base
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
\rewind($fp);
return $this->_sign($fp, $output);
}
protected function _verify($input, string $signature)
protected function _verify($input, string $signature) /*: array|false*/
{
$arguments = ['--verify'];
if ($signature) {
@ -768,6 +800,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
// \rewind($fp);
return $this->_verify($fp, $signature);
}

View file

@ -11,6 +11,8 @@
namespace SnappyMail\GPG;
use SnappyMail\SensitiveString;
class SMIME extends Base
{
private
@ -235,9 +237,9 @@ class SMIME extends Base
return false;
}
protected function _exportKey($keyId, $private = false)
protected function _exportKey($keyId, bool $private = false)
{
$keys = $this->keyInfo($keyId, $private ? 1 : 0);
$keys = $this->keyInfo($keyId, $private);
if (!$keys) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
}
@ -253,21 +255,18 @@ class SMIME extends Base
}
/**
* Exports a public key
* Exports a public or private key
*/
public function export(string $fingerprint) /*: string|false*/
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
{
if ($passphrase) {
return $this
->addPassphrase($fingerprint, $passphrase)
->_exportKey($fingerprint, true);
}
return $this->_exportKey($fingerprint);
}
/**
* Exports a private key
*/
public function exportPrivateKey(string $fingerprint) /*: string|false*/
{
return $this->_exportKey($fingerprint, true);
}
protected function _importKey($input) /*: array|false*/
{
$arguments = ['--import'];
@ -334,7 +333,7 @@ class SMIME extends Base
public function deleteKey(string $keyId, bool $private)
{
$key = $this->keyInfo($keyId, $private ? 1 : 0);
$key = $this->keyInfo($keyId, $private);
if (!$key) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
}
@ -359,7 +358,7 @@ class SMIME extends Base
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function keyInfo(string $pattern, int $private = 0) : array
public function keyInfo(string $pattern, bool $private = false) : array
{
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys.
@ -540,7 +539,7 @@ class SMIME extends Base
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
@ -548,7 +547,7 @@ class SMIME extends Base
return $this->_sign($fp, $output);
}
protected function _verify($input, string $signature)
protected function _verify($input, string $signature) /*: array|false*/
{
$arguments = ['--verify'];
if ($signature) {

View file

@ -7,10 +7,11 @@ abstract class Request
const
/**
* Authentication
* These are bitwise options
*/
AUTH_BASIC = 1,
AUTH_DIGEST = 2,
AUTH_BEARER = 3;
AUTH_BEARER = 4;
public
$timeout = 5, // timeout in seconds.

View file

@ -42,6 +42,10 @@ class Response
if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
$this->body = \gzinflate(\substr($body, 10, -4));
if (false === $this->body) {
$err = \error_get_last() ?: ['message' => 'gzinflate failed'];
throw new \RuntimeException("{$err['message']} for {$request_uri}");
}
} else {
$this->body = $body;
}

View file

@ -8,423 +8,26 @@ defined('GNUPG_SIG_MODE_CLEAR') || define('GNUPG_SIG_MODE_CLEAR', 2);
use SnappyMail\GPG\PGP as GPG;
class GnuPG
abstract class GnuPG
{
private
$homedir,
// Instance of gnupg pecl extension https://www.php.net/gnupg
$GnuPG,
// Instance of \SnappyMail\GPG\PGP
$GPG;
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;
// \putenv("GNUPGHOME={$homedir}");
if (\class_exists('gnupg') && \version_compare(\phpversion('gnupg'), '1.5', '>=')) {
$this->GnuPG = new \gnupg([
// It is the file name of the executable program implementing this protocol which is usually path of the gpg executable.
// 'file_name' => '/usr/bin/gpg',
// It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose.
'home_dir' => $homedir
]);
// Output is ASCII
$this->GnuPG->setarmor(1);
} else {
$this->getGPG();
}
/*
$conf = "{$homedir}/gpg-agent.conf";
if (!\file_exists($conf)) {
\file_put_contents($conf, 'default-cache-ttl 1');
}
$conf = "{$homedir}/gpg.conf";
if (!\file_exists($conf)) {
\file_put_contents($conf, "batch\nno-comments");
}
*/
}
public static function isSupported() : bool
{
return \class_exists('gnupg') || GPG::isSupported();
return GPG::isSupported();
}
private static $instance;
public static function getInstance(string $homedir) : ?self
public static function getInstance(string $homedir) : ?PGPInterface
{
if (!static::$instance) {
static::$instance = new self($homedir);
if (GPG::isSupported()) {
static::$instance = new GPG($homedir);
}
/*
else if (PECL::isSupported()) {
static::$instance = new PECL($homedir);
}
*/
}
return static::$instance;
}
public function handler()
{
return $this->GnuPG ?: $this->GPG;
}
public function getGPG(bool $throw = true) : ?GPG
{
if (!$this->GPG) {
if (GPG::isSupported()) {
$this->GPG = new GPG($this->homedir);
} else if ($throw) {
throw new \Exception('GnuPG not supported');
}
}
return $this->GPG;
}
/**
* Add a key for decryption
*/
public function addDecryptKey(string $fingerprint,
#[\SensitiveParameter]
string $passphrase
) : bool
{
return $this->handler()->adddecryptkey($fingerprint, $passphrase);
}
/**
* Add a key for encryption
*/
public function addEncryptKey(string $fingerprint) : bool
{
return $this->handler()->addencryptkey($fingerprint);
}
/**
* Add a key for signing
*/
public function addSignKey(string $fingerprint,
#[\SensitiveParameter]
?string $passphrase
) : bool
{
return $this->handler()->addsignkey($fingerprint, $passphrase);
}
/**
* Removes all keys which were set for decryption before
*/
public function clearDecryptKeys() : bool
{
return $this->handler()->cleardecryptkeys();
}
/**
* Removes all keys which were set for encryption before
*/
public function clearEncryptKeys() : bool
{
return $this->handler()->clearencryptkeys();
}
/**
* Removes all keys which were set for signing before
*/
public function clearSignKeys() : bool
{
return $this->handler()->clearsignkeys();
}
/**
* Decrypts a given text
*/
public function decrypt(string $text) /*: string|false */
{
return $this->GnuPG
? $this->GnuPG->decrypt($text)
: $this->GPG->decrypt($text);
}
/**
* Decrypts a given file
*/
public function decryptFile(string $filename) /*: string|false */
{
return $this->GnuPG
? $this->GnuPG->decrypt(\file_get_contents($filename))
: $this->GPG->decryptFile($filename);
}
/**
* Decrypts a given resource
*/
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
return $this->GnuPG
? $this->GnuPG->decrypt(\stream_get_contents($fp))
: $this->GPG->decryptStream($fp, $output);
}
/**
* Decrypts and verifies a given text
*/
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
{
return $this->GnuPG
? $this->GnuPG->decryptverify($text, $plaintext)
: $this->GPG->decryptverify($text, $plaintext);
}
/**
* Decrypts and verifies a given file
*/
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
{
return $this->GnuPG
? $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext)
: $this->GPG->decryptverifyFile($filename, $plaintext);
}
public function deleteKey(string $keyId, bool $private) : bool
{
return $this->getGPG()->deleteKey($keyId, $private);
}
/**
* Encrypts a given text
*/
public function encrypt(string $plaintext) /*: string|false*/
{
return $this->GnuPG
? $this->GnuPG->encrypt($plaintext)
: $this->GPG->encrypt($plaintext);
}
/**
* Encrypts a given text
*/
public function encryptFile(string $filename) /*: string|false*/
{
return $this->GnuPG
? $this->GnuPG->encrypt(\file_get_contents($filename))
: $this->GPG->encryptFile($filename);
}
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
{
\rewind($fp);
return $this->GnuPG
? $this->GnuPG->encrypt(\stream_get_contents($fp))
: $this->GPG->encryptStream($fp);
}
/**
* Exports a key
*/
public function export(string $fingerprint,
#[\SensitiveParameter]
string $passphrase = ''
) /*: string|false*/
{
if ($passphrase) {
return $this->getGPG()
->addPassphrase($fingerprint, $passphrase)
->exportPrivateKey($fingerprint);
}
return $this->GnuPG
? $this->GnuPG->export($fingerprint)
: $this->GPG->export($fingerprint);
}
/**
* Returns the engine info
*/
public function getEngineInfo() : array
{
return $this->handler()->getengineinfo();
}
/**
* Returns the errortext, if a function fails
*/
public function getError() /*: string|false*/
{
return $this->handler()->geterror();
}
/**
* Returns the error info
*/
public function getErrorInfo() : array
{
return $this->handler()->geterrorinfo();
}
/**
* Returns the currently active protocol for all operations
*/
public function getProtocol() : int
{
return $this->handler()->getprotocol();
}
/**
* Generates a key
*/
public function generateKey(string $uid,
#[\SensitiveParameter]
string $passphrase
) /*: string|false*/
{
$GPG = $this->getGPG(false);
return $GPG ? $GPG->generateKey($uid, $passphrase) : false;
}
/**
* Imports a key
*/
public function import(string $keydata) /*: array|false*/
{
return $this->handler()->import($keydata);
}
/**
* Imports a key
*/
public function importFile(string $filename) /*: array|false*/
{
return $this->GnuPG
? $this->GnuPG->import(\file_get_contents($filename))
: $this->GPG->importFile($filename);
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function keyInfo(string $pattern) : array
{
$keys = [
'public' => [],
'private' => []
];
// Public
foreach (($this->handler()->keyinfo($pattern) ?: []) as $key) {
$key['can_verify'] = $key['can_sign'];
unset($key['can_sign']);
$keys['public'][] = $key;
}
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
foreach (($this->handler()->keyinfo($pattern, 1) ?: []) as $key) {
$key['can_decrypt'] = $key['can_encrypt'];
unset($key['can_encrypt']);
$keys['private'][] = $key;
}
return $keys;
}
/**
* Toggle armored output
* When true the output is ASCII
*/
public function setArmor(bool $armor = true) : bool
{
return $this->handler()->setarmor($armor ? 1 : 0);
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
* By default GNUPG_ERROR_SILENT is used.
*/
public function setErrorMode(int $errormode) : void
{
$this->handler()->seterrormode($errormode);
}
/**
* Sets the mode for signing
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
* By default GNUPG_SIG_MODE_CLEAR
*/
public function setSignMode(int $signmode) : bool
{
return $this->handler()->setsignmode($signmode);
}
/**
* Signs a given text
*/
public function sign(string $plaintext) /*: string|false*/
{
return $this->GnuPG
? $this->GnuPG->sign($plaintext)
: $this->GPG->sign($plaintext);
}
/**
* Signs a given file
*/
public function signFile(string $filename) /*: string|false*/
{
return $this->GnuPG
? $this->GnuPG->sign(\file_get_contents($filename))
: $this->GPG->signFile($filename);
}
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
{
\rewind($fp);
return $this->GnuPG
? $this->GnuPG->sign(\stream_get_contents($fp))
: $this->GPG->signStream($fp);
}
/**
* Verifies a signed text
*/
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{
$result = $this->GnuPG
? $this->GnuPG->verify($signed_text, $signature ?: false, $plaintext)
: $this->GPG->verify($signed_text, $signature, $plaintext);
if (!$result) {
if ($this->GnuPG) {
\SnappyMail\Log::notice('GnuPG', 'gnupg_verify() failed: ' . $this->GnuPG->geterror());
\SnappyMail\Log::info('GnuPG', \print_r($this->GnuPG->geterrorinfo(),1));
} else {
\SnappyMail\Log::notice('GPG', 'GPG->verify() failed');
}
}
return $result;
}
/**
* Verifies a signed file
*/
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{
return $this->GnuPG
? $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext)
: $this->GPG->verifyFile($filename, $signature, $plaintext);
}
/**
* Verifies a given resource
*/
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: string|false */
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
return $this->getGPG()->verifyStream($fp, $signature, $plaintext);
}
}

View file

@ -0,0 +1,397 @@
<?php
namespace SnappyMail\PGP;
defined('GNUPG_SIG_MODE_NORMAL') || define('GNUPG_SIG_MODE_NORMAL', 0);
defined('GNUPG_SIG_MODE_DETACH') || define('GNUPG_SIG_MODE_DETACH', 1);
defined('GNUPG_SIG_MODE_CLEAR') || define('GNUPG_SIG_MODE_CLEAR', 2);
use SnappyMail\GPG\PGP as GPG;
use SnappyMail\SensitiveString;
class PECL implements \SnappyMail\PGP\PGPInterface
{
private
$homedir,
// Instance of gnupg pecl extension https://www.php.net/gnupg
$GnuPG,
// Instance of \SnappyMail\GPG\PGP
$GPG;
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;
// \putenv("GNUPGHOME={$homedir}");
$this->GnuPG = new \gnupg([
// It is the file name of the executable program implementing this protocol which is usually path of the gpg executable.
// 'file_name' => '/usr/bin/gpg',
// It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose.
'home_dir' => $homedir
]);
// Output is ASCII
$this->GnuPG->setarmor(1);
/*
$conf = "{$homedir}/gpg-agent.conf";
if (!\file_exists($conf)) {
\file_put_contents($conf, 'default-cache-ttl 1');
}
$conf = "{$homedir}/gpg.conf";
if (!\file_exists($conf)) {
\file_put_contents($conf, "batch\nno-comments");
}
*/
}
function __destruct()
{
$this->GnuPG->cleardecryptkeys();
$this->GnuPG->clearsignkeys();
}
public static function isSupported() : bool
{
return \class_exists('gnupg') && \version_compare(\phpversion('gnupg'), '1.5', '>=');
}
public function gnupgError()
{
$error = $this->GnuPG->geterrorinfo();
if ($error) {
throw new \Exception("{$error['gpgme_source']} {$error['generic_message']}. {$error['gpgme_message']}", $error['gpgme_code']);
}
}
public function getGPG(bool $throw = true) : ?GPG
{
if (!$this->GPG) {
if (GPG::isSupported()) {
$this->GPG = new GPG($this->homedir);
} else if ($throw) {
throw new \Exception('GPG not supported');
}
}
return $this->GPG;
}
/**
* Add a key for decryption
*/
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool
{
// \SnappyMail\Log::debug('GnuPG', "addDecryptKey({$fingerprint}, {$passphrase})");
return $this->GnuPG->adddecryptkey($fingerprint, \strval($passphrase)) || $this->gnupgError();
}
/**
* Add a key for encryption
*/
public function addEncryptKey(string $fingerprint) : bool
{
return $this->GnuPG->addencryptkey($fingerprint) || $this->gnupgError();
}
/**
* Add a key for signing
*/
public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool
{
// \SnappyMail\Log::debug('GnuPG', "addSignKey({$fingerprint}, {$passphrase})");
return $this->GnuPG->addsignkey($fingerprint, \strval($passphrase)) || $this->gnupgError();
}
/**
* Removes all keys which were set for decryption before
*/
public function clearDecryptKeys() : bool
{
return $this->GnuPG->cleardecryptkeys();
}
/**
* Removes all keys which were set for encryption before
*/
public function clearEncryptKeys() : bool
{
return $this->GnuPG->clearencryptkeys();
}
/**
* Removes all keys which were set for signing before
*/
public function clearSignKeys() : bool
{
return $this->GnuPG->clearsignkeys();
}
/**
* Decrypts a given text
*/
public function decrypt(string $text) /*: string|false */
{
$result = $this->GnuPG->decrypt($text);
(false === $result) && $this->gnupgError();
return $result;
}
/**
* Decrypts a given file
*/
public function decryptFile(string $filename) /*: string|false */
{
$result = $this->GnuPG->decrypt(\file_get_contents($filename));
(false === $result) && $this->gnupgError();
return $result;
}
/**
* Decrypts a given resource
*/
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
// \rewind($fp);
$result = $this->GnuPG->decrypt(\stream_get_contents($fp));
(false === $result) && $this->gnupgError();
return $result;
}
/**
* Decrypts and verifies a given text
*/
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
{
$result = $this->GnuPG->decryptverify($text, $plaintext);
(false === $result) && $this->gnupgError();
return $result;
}
/**
* Decrypts and verifies a given file
*/
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
{
$result = $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext);
(false === $result) && $this->gnupgError();
return $result;
}
public function deleteKey(string $keyId, bool $private) : bool
{
return $this->getGPG()->deleteKey($keyId, $private);
}
/**
* Encrypts a given text
*/
public function encrypt(string $plaintext) /*: string|false*/
{
return $this->GnuPG->encrypt($plaintext) ?: $this->gnupgError();
}
/**
* Encrypts a given text
*/
public function encryptFile(string $filename) /*: string|false*/
{
return $this->GnuPG->encrypt(\file_get_contents($filename)) ?: $this->gnupgError();
}
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
{
\rewind($fp);
return $this->GnuPG->encrypt(\stream_get_contents($fp)) ?: $this->gnupgError();
}
/**
* Exports a key
*/
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
{
if (null !== $passphrase) {
return $this->getGPG()->export($fingerprint, $passphrase);
}
return $this->GnuPG->export($fingerprint) ?: $this->gnupgError();
}
/**
* Returns the engine info
*/
public function getEngineInfo() : array
{
return $this->GnuPG->getengineinfo();
}
/**
* Returns the errortext, if a function fails
*/
public function getError() /*: string|false*/
{
return $this->GnuPG->geterror();
}
/**
* Returns the error info
*/
public function getErrorInfo() : array
{
return $this->GnuPG->geterrorinfo();
}
/**
* Returns the currently active protocol for all operations
*/
public function getProtocol() : int
{
return $this->GnuPG->getprotocol();
}
/**
* Generates a key
*/
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/
{
$GPG = $this->getGPG(false);
return $GPG ? $GPG->generateKey($uid, $passphrase) : false;
}
/**
* Imports a key
*/
public function import(string $keydata) /*: array|false*/
{
return $this->GnuPG->import($keydata);
}
/**
* Imports a key
*/
public function importFile(string $filename) /*: array|false*/
{
return $this->GnuPG->import(\file_get_contents($filename)) ?: $this->gnupgError();
}
public function keyInfo(string $pattern, bool $private = false) : array
{
return $this->GnuPG->keyinfo($pattern, $private ? 1 : 0);
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function allKeysInfo(string $pattern) : array
{
$keys = [
'public' => [],
'private' => []
];
// Public
foreach (($this->GnuPG->keyinfo($pattern) ?: []) as $key) {
$key['can_verify'] = $key['can_sign'];
unset($key['can_sign']);
$keys['public'][] = $key;
}
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
foreach (($this->GnuPG->keyinfo($pattern, 1) ?: []) as $key) {
$key['can_decrypt'] = $key['can_encrypt'];
unset($key['can_encrypt']);
$keys['private'][] = $key;
}
return $keys;
}
/**
* Toggle armored output
* When true the output is ASCII
*/
public function setArmor(bool $armor = true) : bool
{
return $this->GnuPG->setarmor($armor ? 1 : 0);
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
* By default GNUPG_ERROR_SILENT is used.
*/
public function setErrorMode(int $errormode) : void
{
$this->GnuPG->seterrormode($errormode);
}
/**
* Sets the mode for signing
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
* By default GNUPG_SIG_MODE_CLEAR
*/
public function setSignMode(int $signmode) : bool
{
return $this->GnuPG->setsignmode($signmode);
}
/**
* Signs a given text
*/
public function sign(string $plaintext) /*: string|false*/
{
return $this->GnuPG->sign($plaintext) ?: $this->gnupgError();
}
/**
* Signs a given file
*/
public function signFile(string $filename) /*: string|false*/
{
return $this->GnuPG->sign(\file_get_contents($filename)) ?: $this->gnupgError();
}
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
{
\rewind($fp);
return $this->GnuPG->sign(\stream_get_contents($fp)) ?: $this->gnupgError();
}
/**
* Verifies a signed text
*/
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{
$result = $this->GnuPG->verify($signed_text, $signature ?: false, $plaintext) ?: $this->gnupgError();
if (!$result) {
\SnappyMail\Log::notice('GnuPG', 'gnupg_verify() failed: ' . $this->GnuPG->geterror());
\SnappyMail\Log::info('GnuPG', \print_r($this->GnuPG->geterrorinfo(),1));
}
return $result;
}
/**
* Verifies a signed file
*/
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{
return $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext) ?: $this->gnupgError();
}
/**
* Verifies a given resource
*/
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: array|false */
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
// \rewind($fp);
return $this->GnuPG->verify(\stream_get_contents($fp), $signature, $plaintext) ?: $this->gnupgError();
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace SnappyMail\PGP;
use SnappyMail\SensitiveString;
interface PGPInterface
{
public static function isSupported() : bool;
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool;
public function addEncryptKey(string $fingerprint) : bool;
public function addSignKey(string $fingerprint, SensitiveString $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, ?SensitiveString $passphrase = null) /*: string|false*/;
public function getEngineInfo() : array;
public function getError() /*: string|false*/;
public function getErrorInfo() : array;
public function getProtocol() : int;
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/;
public function import(string $keydata) /*: array|false*/;
public function importFile(string $filename) /*: array|false*/;
public function allKeysInfo(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) /*: string|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) /*: array|false */;
}

View file

@ -2,7 +2,7 @@
namespace SnappyMail;
class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable
class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable, \JsonSerializable
{
private string $value, $nonce;
private static ?string $key = null;
@ -28,6 +28,7 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
string $value
) : void
{
\strlen($value) && \RainLoop\Api::Actions()->logMask($value);
if (\is_callable('sodium_crypto_secretbox')) {
$this->nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
if (!static::$key) {
@ -60,6 +61,12 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
return $this->getValue();
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
throw new \Exception("JSON serialization of 'SnappyMail\\SensitiveString' is not allowed");
}
public function __debugInfo(): array
{
return [];

View file

@ -112,7 +112,7 @@ class Certificate
return \openssl_get_cipher_methods($aliases);
}
public function createSelfSigned(string $passphrase = '') : array
public function createSelfSigned(\SnappyMail\SensitiveString $passphrase, ?string $privateKey = null) : array
{
$options = array(
'config' => __DIR__ . '/openssl.cnf',
@ -134,7 +134,13 @@ class Certificate
}
$pkey = null; // openssl_pkey_new($options);
$csr = \openssl_csr_new($dn, $pkey, $options);
if ($privateKey) {
$pkey = \openssl_pkey_get_private($privateKey, $passphrase);
if (!$pkey) {
throw new \RuntimeException('OpenSSL pkey: ' . \openssl_error_string());
}
}
$csr = \openssl_csr_new($dn, $pkey, $options);
if ($csr) {
$this->x509 = \openssl_csr_sign(
$csr,
@ -145,13 +151,11 @@ class Certificate
);
if ($this->x509/* && $this->canSign() && $this->canEncrypt()*/) {
$this->pkey = $pkey;
$privatekey = '';
$privateKey || \openssl_pkey_export($pkey, $privateKey, $passphrase);
$certificate = '';
$csrStr = '';
\openssl_pkey_export($pkey, $privatekey, $passphrase);
\openssl_x509_export($this->x509, $certificate);
return array(
'pkey' => $privatekey,
'pkey' => $privateKey,
'x509' => $certificate,
// 'pkcs12' => $this->asPKCS12($pkey, $passphrase/*, array $args = array()*/)
// 'canSign' => $this->canSign(),
@ -167,7 +171,11 @@ class Certificate
}
// returns binary data
public function asPKCS12(string $pass = '', array $args = array()) : string
public function asPKCS12(
#[\SensitiveParameter]
string $pass = '',
array $args = array()
) : string
{
$out = '';
\openssl_pkcs12_export($this->x509, $out, $this->pkey, $pass, $args);

View file

@ -34,15 +34,18 @@ class OpenSSL
{
$data = \openssl_x509_parse(\openssl_x509_read($certificate));
if (!$data) {
\error_log("OpenSSL parse: " . \openssl_error_string());
\SnappyMail\Log::error('OpenSSL', "parse: " . \openssl_error_string());
return false;
}
$key = \str_replace(':', '', $data['extensions']['subjectKeyIdentifier'] ?? $data['hash']);
$filename = "{$this->homedir}/{$key}.crt";
if (!\file_exists($filename)) {
if (\file_exists($filename)) {
\SnappyMail\Log::debug('OpenSSL', "certificate {$key} already imported");
} else {
\file_put_contents("{$this->homedir}/{$key}.crt", $certificate);
// \unlink("{$this->homedir}/certificates.json");
$this->certificates(true);
\SnappyMail\Log::debug('OpenSSL', "certificate {$key} imported");
}
return true;
}
@ -50,7 +53,7 @@ class OpenSSL
public function certificates(bool $force = false) : array
{
$cacheFile = "{$this->homedir}/certificates.json";
$result = \file_exists($cacheFile)
$result = (!$force && \file_exists($cacheFile))
? \json_decode(\file_get_contents($cacheFile), true)
: null;
if (!\is_array($result)) {
@ -98,7 +101,7 @@ class OpenSSL
}
$result[] = $short;
} else {
\error_log("OpenSSL parse({$file}): " . \openssl_error_string());
\SnappyMail\Log::error('OpenSSL', "parse({$file}): " . \openssl_error_string());
}
}
\file_put_contents($cacheFile, \json_encode($result));
@ -129,8 +132,7 @@ class OpenSSL
}
public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey,
#[\SensitiveParameter]
?string $passphrase = null
?\SnappyMail\SensitiveString $passphrase = null
) : void
{
$this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase);
@ -142,6 +144,19 @@ class OpenSSL
}
}
/*
public function asn1parse(/*string|Temporary* / $input) : ?string
{
if (\is_string($input)) {
$tmp = new Temporary('smimein-');
if (!$tmp->putContents($input)) {
return null;
}
$input = $tmp;
}
`openssl asn1parse -in $input->filename()`
}
*/
public function decrypt(/*string|Temporary*/ $input) : ?string
{
if (\is_string($input)) {

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "توقيع جيد من %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP استيراد مفتاح",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Валиден подпис за %USER%",
"ERROR": "%TYPE% грешка: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Внасяне на OpenPGP ключ",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Подписано с S\/MIME",
"ENCRYPTED_MESSAGE": "Шифровано с S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Ověřený podpis od %USER%",
"ERROR": "%TYPE% chyba: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import OpenPGP klíče",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Zpráva podepsaná S\/MIME",
"ENCRYPTED_MESSAGE": "Zpráva šifrovaná S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Accepteret signatur for %USER\"",
"ERROR": "%TYPE% fejl: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importer OpenPGP nøgle",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signeret meddelelse",
"ENCRYPTED_MESSAGE": "S\/MIME krypteret meddelelse",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Gültige Unterschrift von %USER%",
"ERROR": "%TYPE%-Fehler: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP-Schlüssel importieren",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signierte Nachricht",
"ENCRYPTED_MESSAGE": "S\/MIME-verschlüsselte Nachricht",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Εισαγωγή κλειδιού OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Μήνυμα υπογεγραμμένο με S\/MIME",
"ENCRYPTED_MESSAGE": "Μήνυμα κωδικοποιημένο με S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S/MIME Certificates",
"SIGNED_MESSAGE": "S/MIME signed message",
"ENCRYPTED_MESSAGE": "S/MIME encrypted message",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Buena firma de %USER%",
"ERROR": "Error %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importar clave OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensaje firmado mediante S\/MIME",
"ENCRYPTED_MESSAGE": "Mensaje cifrado mediante S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Korralik signatuur kasutajalt %USER%",
"ERROR": "%TYPE% viga: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Impordi OpenPGP võti",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME abil signeeritud kiri",
"ENCRYPTED_MESSAGE": "S\/MIME abil krüpteeritud kiri",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "%USER%-en sinadura egokia",
"ERROR": "%TYPE% errorea: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Inportatu OpenPGP gakoa",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME sinatutako mezua",
"ENCRYPTED_MESSAGE": "S\/MIME zifratutako mezua",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "امضای صحیح از %USER%",
"ERROR": "خطای %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "واردکردن کلید OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "پیام توسط S\/MIME امضاء شد",
"ENCRYPTED_MESSAGE": "پیام توسط S\/MIME رمزنگاری شد",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Oikea allekirjoitus %USER% lle",
"ERROR": "%TYPE% virhe: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Tuo OpenPGP avain",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME allekirjoitettu viesti",
"ENCRYPTED_MESSAGE": "S\/MIME salattu visti",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Signature valide pour %USER%",
"ERROR": "Erreur %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importer la clef OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Message signé par S\/MIME",
"ENCRYPTED_MESSAGE": "Message chiffré par S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Jó aláírás tőle: %USER%",
"ERROR": "%TYPE% hiba: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP kulcs importálás",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME aláírt üzenet",
"ENCRYPTED_MESSAGE": "S\/MIME kódolt üzenet",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Tandatangan sah dari user %USER%",
"ERROR": "Kesalahan %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Impor kunci OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Pesan bertanda-tangan S\/MIME",
"ENCRYPTED_MESSAGE": "Pesan terenkripsi S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Gild undirritun frá %USER%",
"ERROR": "%TYPE% villa: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Flytja inn OpenPGP-lykil",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Skeyti undirritað með S\/MIME",
"ENCRYPTED_MESSAGE": "Skeyti dulritað með S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Firma valida da %USER%",
"ERROR": "Errore di %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importa chiave OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Messaggio firmato con S\/MIME",
"ENCRYPTED_MESSAGE": "Messaggio cifrato con S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "%USER%からの正しい署名",
"ERROR": "%TYPE% エラー: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGPキーをインポート",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME署名済みメッセージ",
"ENCRYPTED_MESSAGE": "S\/MIME暗号化メッセージ",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "%USER%(으)로부터 수신한 양호한 서명",
"ERROR": "%TYPE% 오류: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP 키 가져오기",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME로 서명된 메세지입니다.",
"ENCRYPTED_MESSAGE": "S\/MIME로 암호화된 메세지입니다.",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Tinkamas parašas nuo %USER%",
"ERROR": "%TYPE% klaida: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importuoti OpenPGP raktą",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME pasirašytas pranešimas",
"ENCRYPTED_MESSAGE": "S\/MIME šifruotas pranešimas",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Gyldig signatur fra %USER%",
"ERROR": "%TYPE%-feil: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importer OpenPGP-nøkkel",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signert melding",
"ENCRYPTED_MESSAGE": "S\/MIME-kryptert melding",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Juiste handtekening van %USER%",
"ERROR": "%TYPE% foutmelding: %ERROR%",
"CREATE_SELF_SIGNED": "Zelfondertekend certificaat aanmaken",
"VALID_UNTIL": "Geldig tot"
"VALID_UNTIL": "Geldig tot",
"PRIVATE_KEY": "Privésleutel"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importeer OpenPGP sleutel",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Importeer S\/MIME certificaat",
"CERTIFICATE": "Certificaat",
"CERTIFICATES": "S\/MIME Certificaten",
"SIGNED_MESSAGE": "S\/MIME ondertekend bericht",
"ENCRYPTED_MESSAGE": "S\/MIME versleuteld bericht",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Poprawnie podpisana przez %USER%",
"ERROR": "Błąd %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Utwórz samodzielnie podpisany certyfikat",
"VALID_UNTIL": "Ważny do"
"VALID_UNTIL": "Ważny do",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importowanie klucza OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Importuj certyfikat S\/MIME",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "Certyfikaty S\/MIME",
"SIGNED_MESSAGE": "Wiadomość podpisana S\/MIME",
"ENCRYPTED_MESSAGE": "Wiadomość zaszyfrowana S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
"ERROR": "Erro %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem criptografada com S\/MIME",

View file

@ -154,8 +154,8 @@
"HAS_VIRUS_WARNING": "AVISO: vírus detetado",
"TAGS": "Etiquetas",
"NEW_TAG": "Nova etiqueta",
"ENABLE_TRACKING_LINKS": "Enable tracking links",
"TRACKING_ENABLED": "Links are now tracked when clicked!"
"ENABLE_TRACKING_LINKS": "Ativar ligações de rastreamento",
"TRACKING_ENABLED": "As ligações irão ser rastreadas quando clicadas!"
},
"MESSAGE_TAGS": {
"$important": "Importante",
@ -255,7 +255,7 @@
"POPUPS_IDENTITY": {
"TITLE_ADD_IDENTITY": "Adicionar identidade?",
"TITLE_UPDATE_IDENTITY": "Atualizar identidade?",
"LABEL": "Label",
"LABEL": "Rótulo",
"BUTTON_ADD_IDENTITY": "Adicionar",
"BUTTON_UPDATE_IDENTITY": "Atualizar",
"SIGNATURE": "Assinatura",
@ -281,8 +281,9 @@
"VERIFY": "Validar",
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
"ERROR": "Erro %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"CREATE_SELF_SIGNED": "Criar auto-assinado",
"VALID_UNTIL": "Válido até",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
@ -295,18 +296,19 @@
"SIGNED_MESSAGE": "Mensagem assinada com OpenPGP",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com OpenPGP",
"STORE_IN_GNUPG": "Guardar no servidor em GnuPG",
"STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG",
"STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG",
"STORE_PUBLIC_KEY_IN_GNUPG": "Guardar a chave pública no servidor em GnuPG",
"STORE_PRIVATE_KEY_IN_GNUPG": "Guardar a chave privada no servidor em GnuPG",
"BACKUP_ON_SERVER": "Guardar (encriptado) no servidor",
"BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server",
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
"BACKUP_PUBLIC_KEY_ON_SERVER": "Fazer cópia de segurança da chave pública no servidor",
"BACKUP_PRIVATE_KEY_ON_SERVER": "Fazer cópia de segurança da chave privada no servidor"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"POPUP_IMPORT_TITLE": "Importar certificado S\/MIME",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "Certificados S\/MIME",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",
"PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%"
"PRIVATE_KEY_OF": "Chave privada S\/MIME de %EMAIL%"
},
"POPUPS_FILTER": {
"TITLE_CREATE_FILTER": "Criar um filtro?",
@ -405,7 +407,7 @@
"IMAGEUPLOAD": "Escolher imagem",
"UNDO": "Desfazer",
"REDO": "Refazer",
"REMOVESTYLE": "Remove style"
"REMOVESTYLE": "Remover estilo"
},
"SETTINGS_LABELS": {
"GENERAL": "Geral",
@ -530,7 +532,7 @@
"SETTINGS_OPENPGP": {
"BUTTON_IMPORT_KEY": "Importar chave",
"BUTTON_GENERATE_KEY_PAIR": "Gerar chaves",
"IMPORT_FROM_SERVER": "Import from server",
"IMPORT_FROM_SERVER": "Importar de servidor",
"TITLE_PRIVATE": "Privada",
"TITLE_PUBLIC": "Pública",
"GET_MAILVELOPE": "Obter extensão Mailvelope para o navegador."

View file

@ -154,8 +154,8 @@
"HAS_VIRUS_WARNING": "AVISO: vírus detetado",
"TAGS": "Etiquetas",
"NEW_TAG": "Nova etiqueta",
"ENABLE_TRACKING_LINKS": "Enable tracking links",
"TRACKING_ENABLED": "Links are now tracked when clicked!"
"ENABLE_TRACKING_LINKS": "Ativar ligações de rastreamento",
"TRACKING_ENABLED": "As ligações irão ser rastreadas quando clicadas!"
},
"MESSAGE_TAGS": {
"$important": "Importante",
@ -255,7 +255,7 @@
"POPUPS_IDENTITY": {
"TITLE_ADD_IDENTITY": "Adicionar identidade?",
"TITLE_UPDATE_IDENTITY": "Atualizar identidade?",
"LABEL": "Label",
"LABEL": "Rótulo",
"BUTTON_ADD_IDENTITY": "Adicionar",
"BUTTON_UPDATE_IDENTITY": "Atualizar",
"SIGNATURE": "Assinatura",
@ -281,8 +281,9 @@
"VERIFY": "Validar",
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
"ERROR": "Erro %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"CREATE_SELF_SIGNED": "Criar auto-assinado",
"VALID_UNTIL": "Válido até",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
@ -295,18 +296,19 @@
"SIGNED_MESSAGE": "Mensagem assinada com OpenPGP",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com OpenPGP",
"STORE_IN_GNUPG": "Guardar no servidor em GnuPG",
"STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG",
"STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG",
"STORE_PUBLIC_KEY_IN_GNUPG": "Guardar a chave pública no servidor em GnuPG",
"STORE_PRIVATE_KEY_IN_GNUPG": "Guardar a chave privada no servidor em GnuPG",
"BACKUP_ON_SERVER": "Guardar (encriptado) no servidor",
"BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server",
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
"BACKUP_PUBLIC_KEY_ON_SERVER": "Fazer cópia de segurança da chave pública no servidor",
"BACKUP_PRIVATE_KEY_ON_SERVER": "Fazer cópia de segurança da chave privada no servidor"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"POPUP_IMPORT_TITLE": "Importar certificado S\/MIME",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "Certificados S\/MIME",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",
"PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%"
"PRIVATE_KEY_OF": "Chave privada S\/MIME de %EMAIL%"
},
"POPUPS_FILTER": {
"TITLE_CREATE_FILTER": "Criar um filtro?",
@ -405,7 +407,7 @@
"IMAGEUPLOAD": "Escolher imagem",
"UNDO": "Desfazer",
"REDO": "Refazer",
"REMOVESTYLE": "Remove style"
"REMOVESTYLE": "Remover estilo"
},
"SETTINGS_LABELS": {
"GENERAL": "Geral",
@ -530,7 +532,7 @@
"SETTINGS_OPENPGP": {
"BUTTON_IMPORT_KEY": "Importar chave",
"BUTTON_GENERATE_KEY_PAIR": "Gerar chaves",
"IMPORT_FROM_SERVER": "Import from server",
"IMPORT_FROM_SERVER": "Importar de servidor",
"TITLE_PRIVATE": "Privada",
"TITLE_PUBLIC": "Pública",
"GET_MAILVELOPE": "Obter extensão Mailvelope para o navegador."

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Подтвержденная сигнатура для %USER%",
"ERROR": "%TYPE% ошибка: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Импорт OpenPGP ключа",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME подписанное сообщение",
"ENCRYPTED_MESSAGE": "S\/MIME шифрованное сообщение",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Good signature from %USER%",
"ERROR": "%TYPE% error: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importovať kľúč OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Správa podpísaná s S\/MIME",
"ENCRYPTED_MESSAGE": "Správa šifrovaná s S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Dober podpis od %USER%",
"ERROR": "%TYPE% napaka: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Uvoz OpenPGP ključa",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Sporočilo, podpisano z S\/MIME",
"ENCRYPTED_MESSAGE": "Sporočilo, šifrirano z S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Godkänd signatur för %USER%",
"ERROR": "%TYPE%-fel: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Import av OpenPGP-nyckel",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signerat meddelande",
"ENCRYPTED_MESSAGE": "S\/MIME-krypterat meddelande",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Geçerli imza %USER%",
"ERROR": "%TYPE% hatası: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP key'i içe aktar",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME imzalı mesaj",
"ENCRYPTED_MESSAGE": "S\/MIME şifreli mesaj",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Підтверджена сигнатура для %USER%",
"ERROR": "%TYPE% помилка: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Імпорт OpenPGP ключа",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME підписане повідомлення",
"ENCRYPTED_MESSAGE": "S\/MIME шифроване повідомлення",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Chữ ký ổn từ %USER%",
"ERROR": "Lỗi %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Nhập mật mã OpenPGP",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Thư đã được ký xác nhận bằng mã S\/MIME",
"ENCRYPTED_MESSAGE": "Thư đã được mã hóa bởi S\/MIME",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "来自 %USER% 的可用签名",
"ERROR": "%TYPE% 错误: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "导入 OpenPGP 密钥",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "由 S\/MIME 签名",
"ENCRYPTED_MESSAGE": "由 S\/MIME 加密",

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "來自「%USER%」的良好簽署",
"ERROR": "%TYPE% 錯誤: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until"
"VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "匯入 OpenPGP 金鑰",
@ -303,6 +304,7 @@
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "已透過 S\/MIME 簽署郵件",
"ENCRYPTED_MESSAGE": "已透過 S\/MIME 加密郵件",

View file

@ -89,14 +89,14 @@
<details>
<summary><strong>S/MIME</strong></summary>
<div class="control-group">
<label>Private key</label>
<label data-i18n="CRYPTO/PRIVATE_KEY"></label>
<textarea name="smimeKey" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeKey"></textarea>
</div>
<div class="control-group">
<label>Certificate</label>
<label data-i18n="SMIME/CERTIFICATE"></label>
<textarea name="smimeCertificate" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeCertificate"></textarea>
</div>
<div class="control-group" data-bind="hidden:smimeKey">
<div class="control-group" data-bind="hidden:smimeCertificate">
<label></label>
<button type="button" data-bind="click: $root.createSelfSigned" data-i18n="CRYPTO/CREATE_SELF_SIGNED"></button>
</div>