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 ## 2.35.0 2024-02-20
### Added ### Added

View file

@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.170.153 | 81.641 | |admin.js |2.170.153 | 82.445 |
|app.js |4.207.787 | 426.852 | |app.js |4.207.787 | 429.565 |
|boot.js | 868.735 | 4.142 | |boot.js | 868.735 | 4.142 |
|libs.js | 658.812 | 192.625 | |libs.js | 658.812 | 192.786 |
|sieve.js | 0 | 84.707 | |sieve.js | 0 | 84.707 |
|polyfills.js | 334.608 | 0 | |polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |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 | |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 | |admin.min.js | 256.831 | 40.573 | 73.606 | 13.585 | 60.877 | 12.188 |
|app.min.js | 515.367 | 193.738 |139.456 | 65.570 |110.485 | 56.281 | |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 | |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 | |sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | |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 |1.217.635 | 289.843 |358.761 |101.789 |299.485 | 88.516 |
|TOTAL user+sieve |1.217.635 | 329.650 |358.761 |111.551 |299.485 | 97.493 | |TOTAL user+sieve |1.217.635 | 331.007 |358.761 |112.154 |299.485 | 97.875 |
|TOTAL admin | 959.099 | 134.890 |292.911 | 49.061 |249.877 | 43.900 | |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. 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 | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 84.435 | 46.946 | 17.612 | 15.091 | |app.css | 340.331 | 84.472 | 46.946 | 17.622 | 15.112 |
|app.min.css | 274.947 | 67.825 | 39.647 | 15.526 | 13.540 | |app.min.css | 274.947 | 67.857 | 39.647 | 15.537 | 13.549 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.576 | | 7.013 | 6.096 | |admin.css | | 30.576 | | 7.013 | 6.096 |

View file

@ -69,10 +69,10 @@ export const GnuPGUserStore = new class {
} }
}; };
if (isPrivate) { if (isPrivate) {
key.password = async (btnTxt = 'SIGN') => { key.password = async (btnTxt = 'CRYPTO/SIGN') => {
const pass = await Passphrases.ask(key, const pass = await Passphrases.ask(key,
'GnuPG key<br>' + key.id + ' ' + key.emails[0], 'GnuPG key<br>' + key.id + ' ' + key.emails[0],
'CRYPTO/'+btnTxt btnTxt
); );
pass && pass.remember && Passphrases.set(key, pass.password); pass && pass.remember && Passphrases.set(key, pass.password);
return pass?.password; return pass?.password;
@ -82,8 +82,8 @@ export const GnuPGUserStore = new class {
if (key.armor) { if (key.armor) {
callback && callback(); callback && callback();
} else { } else {
let pass = isPrivate ? await key.password('POPUP_VIEW_TITLE') : ''; let pass = isPrivate ? await key.password('OPENPGP/POPUP_VIEW_TITLE') : '';
if (null != pass) { if (null != pass) try {
const result = await Remote.post('GnupgExportKey', null, { const result = await Remote.post('GnupgExportKey', null, {
keyId: key.id, keyId: key.id,
isPrivate: isPrivate, isPrivate: isPrivate,
@ -95,6 +95,8 @@ export const GnuPGUserStore = new class {
} else { } else {
Passphrases.delete(key); Passphrases.delete(key);
} }
} catch (e) {
Passphrases.delete(key);
} }
} }
return key.armor; return key.armor;
@ -182,15 +184,20 @@ export const GnuPGUserStore = new class {
uid: message.uid, uid: message.uid,
partId: pgpInfo.partId, partId: pgpInfo.partId,
keyId: key.id, keyId: key.id,
passphrase: await key.password('DECRYPT'), passphrase: await key.password('CRYPTO/DECRYPT'),
data: '' // message.plain() optional data: '' // message.plain() optional
} }
if (null !== params.passphrase) { if (null != params.passphrase) {
const result = await Remote.post('GnupgDecrypt', null, params); try {
if (result?.Result?.data) { const response = await Remote.post('GnupgDecrypt', null, params);
return result.Result; 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: hidden;
overflow-y: auto; overflow-y: auto;
min-width: 100px; min-width: 100px;
color: var(--folders-color, #333);
} }
hr { hr {
@ -77,22 +78,18 @@
white-space: nowrap; white-space: nowrap;
a { a {
display: block;
position: relative;
z-index: 1;
line-height: 34px;
background-color: transparent; background-color: transparent;
vertical-align: middle;
color: var(--folders-disabled-color, #666);
border-left: 3px solid transparent; border-left: 3px solid transparent;
color: var(--folders-color, #333);
display: block;
line-height: 34px;
padding: 0 2em 0 @folderItemPadding; padding: 0 2em 0 @folderItemPadding;
position: relative;
text-decoration: none; text-decoration: none;
vertical-align: middle;
z-index: 1;
&.selectable { &.selectable {
color: var(--folders-color, #333);
cursor: pointer; cursor: pointer;
&:hover { &:hover {
@ -115,6 +112,7 @@
} }
} }
&:not(.selectable) { &:not(.selectable) {
color: var(--folders-disabled-color, #666);
cursor: default; cursor: default;
font-style: italic; font-style: italic;
} }

View file

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

View file

@ -587,10 +587,13 @@ export class MailMessageView extends AbstractViewRight {
} }
} else { } else {
// TODO: translate // 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*/) { 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. 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 WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway # 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 && \ 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 && \ unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \ rm /tmp/snappymail.zip && \

View file

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

View file

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

View file

@ -91,7 +91,6 @@ class SnappyMailHelper
*/ */
if ($doLogin && $aCredentials[1] && $aCredentials[2]) { if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
try { try {
$oActions->Logger()->AddSecret($aCredentials[2]);
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if ($oAccount) { if ($oAccount) {
// Must be here due to bug #1241 // Must be here due to bug #1241
@ -202,9 +201,10 @@ class SnappyMailHelper
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt); 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(); 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() # script_snappymail_versions()
sub script_snappymail_versions sub script_snappymail_versions
{ {
return ( "2.35.0" ); return ( "2.35.2" );
} }
sub script_snappymail_version_desc sub script_snappymail_version_desc

View file

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

View file

@ -8,6 +8,7 @@ class LdapConfig
public const CONFIG_SERVER = "server"; public const CONFIG_SERVER = "server";
public const CONFIG_PROTOCOL_VERSION = "server_version"; public const CONFIG_PROTOCOL_VERSION = "server_version";
public const CONFIG_STARTTLS = "starttls"; public const CONFIG_STARTTLS = "starttls";
public const CONFIG_MAIL_PREFIX = "mail_prefix";
public const CONFIG_BIND_USER = "bind_user"; public const CONFIG_BIND_USER = "bind_user";
public const CONFIG_BIND_PASSWORD = "bind_password"; public const CONFIG_BIND_PASSWORD = "bind_password";
@ -30,6 +31,7 @@ class LdapConfig
public $server; public $server;
public $protocol; public $protocol;
public $starttls; public $starttls;
public $mail_prefix;
public $bind_user; public $bind_user;
public $bind_password; public $bind_password;
public $user_base; public $user_base;
@ -51,6 +53,7 @@ class LdapConfig
$ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER)); $ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER));
$ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); $ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3));
$ldap->starttls = (bool)trim($config->Get("plugin", self::CONFIG_STARTTLS)); $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_user = trim($config->Get("plugin", self::CONFIG_BIND_USER));
$ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD));
$ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); $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_base,
$this->config->user_objectclass, $this->config->user_objectclass,
$this->config->user_field_name, $this->config->user_field_name,
$this->config->user_field_mail $this->config->user_field_mail,
$this->config->mail_prefix
); );
} catch (LdapException $e) { } catch (LdapException $e) {
return []; // exceptions are only thrown from the handleerror function that does logging already 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_base,
$this->config->group_objectclass, $this->config->group_objectclass,
$this->config->group_field_name, $this->config->group_field_name,
$this->config->group_field_mail $this->config->group_field_mail,
$this->config->mail_prefix
); );
} catch (LdapException $e) { } catch (LdapException $e) {
return []; // exceptions are only thrown from the handleerror function that does logging already return []; // exceptions are only thrown from the handleerror function that does logging already
@ -241,7 +243,7 @@ class LdapIdentities implements IIdentities
* @return LdapResult[] * @return LdapResult[]
* @throws LdapException * @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(); $this->EnsureBound();
@ -261,6 +263,8 @@ class LdapIdentities implements IIdentities
return []; return [];
} }
$entries = $this->CleanupMailAddresses($entries, $mailField, $mailPrefix);
$results = []; $results = [];
for ($i = 0; $i < $entries["count"]; $i++) { for ($i = 0; $i < $entries["count"]; $i++) {
$entry = $entries[$i]; $entry = $entries[$i];
@ -276,6 +280,49 @@ class LdapIdentities implements IIdentities
return $results; 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 array $entry
* @param string $attribute * @param string $attribute

View file

@ -8,10 +8,10 @@ class LdapIdentitiesPlugin extends AbstractPlugin
{ {
const const
NAME = 'LDAP Identities', NAME = 'LDAP Identities',
VERSION = '2.2', VERSION = '2.3',
AUTHOR = 'FWest98', AUTHOR = 'FWest98',
URL = 'https://github.com/FWest98', URL = 'https://github.com/FWest98',
RELEASE = '2024-02-22', RELEASE = '2024-02-27',
REQUIRED = '2.20.0', REQUIRED = '2.20.0',
CATEGORY = 'Accounts', CATEGORY = 'Accounts',
DESCRIPTION = 'Adds functionality to import account identities from LDAP.'; DESCRIPTION = 'Adds functionality to import account identities from LDAP.';
@ -64,6 +64,12 @@ class LdapIdentitiesPlugin extends AbstractPlugin
->SetDescription("Whether or not to use TLS encrypted connection") ->SetDescription("Whether or not to use TLS encrypted connection")
->SetDefaultValue(true), ->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) Property::NewInstance(LdapConfig::CONFIG_BIND_USER)
->SetLabel("Bind User DN") ->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") ->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'; require_once __DIR__ . '/storage.php';
$oDriver = new \NextcloudStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local'); $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') \RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true), ->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"') \RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false) ->SetDefaultValue(false)

View file

@ -184,7 +184,12 @@ class SieveClient extends \MailSo\Net\NetClient
public function Logout() : void public function Logout() : void
{ {
if ($this->bIsLoggined) { 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; $this->bIsLoggined = false;
} }
} }
@ -408,6 +413,7 @@ class SieveClient extends \MailSo\Net\NetClient
if (null === $sResponseBuffer) { if (null === $sResponseBuffer) {
break; break;
} }
// \MailSo\Imap\Enumerations\ResponseStatus
$bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO')); $bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO'));
// convertEndOfLine // convertEndOfLine
$sLine = \trim($sResponseBuffer); $sLine = \trim($sResponseBuffer);

View file

@ -81,7 +81,7 @@ trait Accounts
$aAccounts = $this->GetAccounts($oMainAccount); $aAccounts = $this->GetAccounts($oMainAccount);
$sEmail = \trim($this->GetActionParam('email', '')); $sEmail = \trim($this->GetActionParam('email', ''));
$sPassword = $this->GetActionParam('password', ''); $oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('password', ''));
$sName = \trim($this->GetActionParam('name', '')); $sName = \trim($this->GetActionParam('name', ''));
$bNew = !empty($this->GetActionParam('new', 1)); $bNew = !empty($this->GetActionParam('new', 1));
@ -92,8 +92,8 @@ trait Accounts
throw new ClientException(Notifications::AccountDoesNotExist); throw new ClientException(Notifications::AccountDoesNotExist);
} }
if ($bNew || $sPassword) { if ($bNew || \strlen($oPassword)) {
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false); $oNewAccount = $this->LoginProcess($sEmail, $oPassword, false);
$aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount); $aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount);
} else { } else {
$aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]); $aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]);
@ -261,7 +261,11 @@ trait Accounts
if (!$oIdentity->FromJSON($this->GetActionParams(), true)) { if (!$oIdentity->FromJSON($this->GetActionParams(), true)) {
throw new ClientException(Notifications::InvalidInputArgument); 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); $this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity);
return $this->TrueResponse(); return $this->TrueResponse();
} }

View file

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

View file

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

View file

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

View file

@ -45,9 +45,10 @@ trait Response
public function ExceptionResponse(\Throwable $oException) : array public function ExceptionResponse(\Throwable $oException) : array
{ {
$iErrorCode = 0; $iErrorCode = Notifications::UnknownError;
$sErrorMessage = ''; $sErrorMessage = $oException->getMessage();
$sErrorMessageAdditional = ''; $sErrorMessageAdditional = '';
$iExceptionCode = 0;
if ($oException instanceof \RainLoop\Exceptions\ClientException) { if ($oException instanceof \RainLoop\Exceptions\ClientException) {
$iErrorCode = $oException->getCode(); $iErrorCode = $oException->getCode();
@ -56,8 +57,7 @@ trait Response
} }
$sErrorMessageAdditional = $oException->getAdditionalMessage(); $sErrorMessageAdditional = $oException->getAdditionalMessage();
} else { } else {
$iErrorCode = Notifications::UnknownError; $iExceptionCode = $oException->getCode();
$sErrorMessage = $oException->getCode().' - '.$oException->getMessage();
} }
$this->logException($oException->getPrevious() ?: $oException); $this->logException($oException->getPrevious() ?: $oException);
@ -65,7 +65,8 @@ trait Response
return $this->DefaultResponse(false, [ return $this->DefaultResponse(false, [
'ErrorCode' => $iErrorCode, 'ErrorCode' => $iErrorCode,
'ErrorMessage' => $sErrorMessage, '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 public function DoSMimeCreateCertificate() : array
{ {
$oAccount = $this->getAccountFromToken(); $oAccount = $this->getAccountFromToken();
$sName = $this->GetActionParam('name', '') ?: $oAccount->Name(); $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$sEmail = $this->GetActionParam('email', '') ?: $oAccount->Email();
$sPassphrase = $this->GetActionParam('passphrase', '');
$this->logMask($sPassphrase);
$cert = new Certificate(); $cert = new Certificate();
$cert->distinguishedName['commonName'] = $sName; $cert->distinguishedName['commonName'] = $this->GetActionParam('name', '') ?: $oAccount->Name();
$cert->distinguishedName['emailAddress'] = $sEmail; $cert->distinguishedName['emailAddress'] = $this->GetActionParam('email', '') ?: $oAccount->Email();
$result = $cert->createSelfSigned($sPassphrase); $result = $cert->createSelfSigned($oPassphrase, $this->GetActionParam('privateKey', ''));
return $this->DefaultResponse($result ?: false); return $this->DefaultResponse($result ?: false);
} }
@ -78,8 +75,7 @@ trait SMime
$sPartId = $this->GetActionParam('partId', ''); $sPartId = $this->GetActionParam('partId', '');
$sCertificate = $this->GetActionParam('certificate', ''); $sCertificate = $this->GetActionParam('certificate', '');
$sPrivateKey = $this->GetActionParam('privateKey', ''); $sPrivateKey = $this->GetActionParam('privateKey', '');
$sPassphrase = $this->GetActionParam('passphrase', ''); $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
$this->logMask($sPassphrase);
$this->initMailClientConnection(); $this->initMailClientConnection();
$oImapClient = $this->ImapClient(); $oImapClient = $this->ImapClient();
@ -106,7 +102,7 @@ trait SMime
$SMIME = $this->SMIME(); $SMIME = $this->SMIME();
$SMIME->setCertificate($sCertificate); $SMIME->setCertificate($sCertificate);
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase); $SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
$result = $SMIME->decrypt($sBody); $result = $SMIME->decrypt($sBody);
return $this->DefaultResponse($result ?: false); return $this->DefaultResponse($result ?: false);
@ -142,9 +138,9 @@ trait SMime
$sBody = \trim($sBody); $sBody = \trim($sBody);
$certificates = []; $certificates = [];
\openssl_pkcs7_read( \openssl_pkcs7_read(
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----", "-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
$certificates $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) { foreach ($certificates as $certificate) {
$this->SMIME()->storeCertificate($certificate); $this->SMIME()->storeCertificate($certificate);
} }
@ -179,7 +175,7 @@ trait SMime
$sBody = \trim($sBody); $sBody = \trim($sBody);
$certificates = []; $certificates = [];
\openssl_pkcs7_read( \openssl_pkcs7_read(
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----", "-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
$certificates $certificates
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,6 +37,16 @@ class Identity implements \JsonSerializable
$this->sEmail = $sEmail; $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 function toMime() : \MailSo\Mime\Email
{ {
return new \MailSo\Mime\Email($this->sEmail, $this->sName); 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'); $this->oActions->logWrite('Action: '.$sMethodName, \LOG_INFO, 'JSON');
$aPost = $_POST ?? null; if ($_POST) {
if ($aPost) { $this->oActions->SetActionParams($_POST, $sMethodName);
$this->oActions->SetActionParams($aPost, $sMethodName); $aPost = $_POST;
foreach ($aPost as $key => $value) { foreach ($aPost as $key => $value) {
if (false !== \stripos($key, 'Password')) { // password & passphrase
if (false !== \stripos($key, 'pass')) {
$aPost[$key] = '*******'; $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'); $this->oActions->logWrite(Utils::jsonEncode($aPost), \LOG_INFO, 'POST');
} else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) { } else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) {
$this->oActions->SetActionParams(array( $this->oActions->SetActionParams(array(
@ -561,14 +553,14 @@ class ServiceActions
(0 === $aData['Time'] || \time() - 10 < $aData['Time'])) (0 === $aData['Time'] || \time() - 10 < $aData['Time']))
{ {
$sEmail = \trim($aData['Email']); $sEmail = \trim($aData['Email']);
$sPassword = $aData['Password']; $oPassword = new \SnappyMail\SensitiveString($aData['Password']);
$aAdditionalOptions = (isset($aData['AdditionalOptions']) && \is_array($aData['AdditionalOptions'])) $aAdditionalOptions = (isset($aData['AdditionalOptions']) && \is_array($aData['AdditionalOptions']))
? $aData['AdditionalOptions'] : []; ? $aData['AdditionalOptions'] : [];
try try
{ {
$oAccount = $this->oActions->LoginProcess($sEmail, $sPassword); $oAccount = $this->oActions->LoginProcess($sEmail, $oPassword);
if ($aAdditionalOptions) { if ($aAdditionalOptions) {
$bNeedToSettings = false; $bNeedToSettings = false;

View file

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

View file

@ -7,7 +7,9 @@
namespace SnappyMail\GPG; namespace SnappyMail\GPG;
class PGP extends Base use SnappyMail\SensitiveString;
class PGP extends Base implements \SnappyMail\PGP\PGPInterface
{ {
private private
$_message, $_message,
@ -95,7 +97,7 @@ class PGP extends Base
$fclose = $this->setOutput($output); $fclose = $this->setOutput($output);
if ($this->decryptKeys) { 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']); $result = $this->exec(['--decrypt','--skip-verify']);
@ -137,6 +139,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
} }
// \rewind($fp);
return $this->_decrypt($fp, $output); return $this->_decrypt($fp, $output);
} }
@ -215,6 +218,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
} }
\rewind($fp);
return $this->_encrypt($fp, $output); return $this->_encrypt($fp, $output);
} }
@ -234,14 +238,14 @@ class PGP extends Base
return false; 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) { if (!$keys) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
} }
if ($private && $this->passphrases) { 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([ $result = $this->exec([
$private ? '--export-secret-keys' : '--export', $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); 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 * Returns the errortext, if a function fails
*/ */
@ -296,8 +298,13 @@ class PGP extends Base
* Also saves revocation certificate in {homedir}/openpgp-revocs.d/ * Also saves revocation certificate in {homedir}/openpgp-revocs.d/
* https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html * 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 = [ $arguments = [
'--batch', '--batch',
'--yes', '--yes',
@ -362,7 +369,7 @@ class PGP extends Base
$arguments = ['--import']; $arguments = ['--import'];
if ($this->passphrases) { if ($this->passphrases) {
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases); $_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->passphrases));
} else { } else {
$arguments[] = '--batch'; $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) { if (!$key) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); 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); // 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 * 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 // According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys. // double '--with-fingerprint' also prints the fingerprint for subkeys.
@ -572,6 +579,30 @@ class PGP extends Base
return $keys; 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 * Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT. * GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
@ -629,7 +660,7 @@ class PGP extends Base
foreach ($this->signKeys as $fingerprint => $pass) { foreach ($this->signKeys as $fingerprint => $pass) {
$arguments[] = '--local-user ' . \escapeshellarg($fingerprint); $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); $result = $this->exec($arguments);
@ -666,15 +697,16 @@ class PGP extends Base
/** /**
* Signs a given file * 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)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
} }
\rewind($fp);
return $this->_sign($fp, $output); return $this->_sign($fp, $output);
} }
protected function _verify($input, string $signature) protected function _verify($input, string $signature) /*: array|false*/
{ {
$arguments = ['--verify']; $arguments = ['--verify'];
if ($signature) { if ($signature) {
@ -768,6 +800,7 @@ class PGP extends Base
if (!$fp || !\is_resource($fp)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
} }
// \rewind($fp);
return $this->_verify($fp, $signature); return $this->_verify($fp, $signature);
} }

View file

@ -11,6 +11,8 @@
namespace SnappyMail\GPG; namespace SnappyMail\GPG;
use SnappyMail\SensitiveString;
class SMIME extends Base class SMIME extends Base
{ {
private private
@ -235,9 +237,9 @@ class SMIME extends Base
return false; 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) { if (!$keys) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); 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); 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*/ protected function _importKey($input) /*: array|false*/
{ {
$arguments = ['--import']; $arguments = ['--import'];
@ -334,7 +333,7 @@ class SMIME extends Base
public function deleteKey(string $keyId, bool $private) public function deleteKey(string $keyId, bool $private)
{ {
$key = $this->keyInfo($keyId, $private ? 1 : 0); $key = $this->keyInfo($keyId, $private);
if (!$key) { if (!$key) {
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); 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 * 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 // According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys. // double '--with-fingerprint' also prints the fingerprint for subkeys.
@ -540,7 +539,7 @@ class SMIME extends Base
/** /**
* Signs a given file * 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)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
@ -548,7 +547,7 @@ class SMIME extends Base
return $this->_sign($fp, $output); return $this->_sign($fp, $output);
} }
protected function _verify($input, string $signature) protected function _verify($input, string $signature) /*: array|false*/
{ {
$arguments = ['--verify']; $arguments = ['--verify'];
if ($signature) { if ($signature) {

View file

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

View file

@ -42,6 +42,10 @@ class Response
if (\function_exists('gzinflate') && isset($this->headers['content-encoding']) if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) { && (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
$this->body = \gzinflate(\substr($body, 10, -4)); $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 { } else {
$this->body = $body; $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; 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 public static function isSupported() : bool
{ {
return \class_exists('gnupg') || GPG::isSupported(); return GPG::isSupported();
} }
private static $instance; private static $instance;
public static function getInstance(string $homedir) : ?self public static function getInstance(string $homedir) : ?PGPInterface
{ {
if (!static::$instance) { 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; 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; namespace SnappyMail;
class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable, \JsonSerializable
{ {
private string $value, $nonce; private string $value, $nonce;
private static ?string $key = null; private static ?string $key = null;
@ -28,6 +28,7 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
string $value string $value
) : void ) : void
{ {
\strlen($value) && \RainLoop\Api::Actions()->logMask($value);
if (\is_callable('sodium_crypto_secretbox')) { if (\is_callable('sodium_crypto_secretbox')) {
$this->nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $this->nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
if (!static::$key) { if (!static::$key) {
@ -60,6 +61,12 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
return $this->getValue(); return $this->getValue();
} }
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
throw new \Exception("JSON serialization of 'SnappyMail\\SensitiveString' is not allowed");
}
public function __debugInfo(): array public function __debugInfo(): array
{ {
return []; return [];

View file

@ -112,7 +112,7 @@ class Certificate
return \openssl_get_cipher_methods($aliases); return \openssl_get_cipher_methods($aliases);
} }
public function createSelfSigned(string $passphrase = '') : array public function createSelfSigned(\SnappyMail\SensitiveString $passphrase, ?string $privateKey = null) : array
{ {
$options = array( $options = array(
'config' => __DIR__ . '/openssl.cnf', 'config' => __DIR__ . '/openssl.cnf',
@ -134,7 +134,13 @@ class Certificate
} }
$pkey = null; // openssl_pkey_new($options); $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) { if ($csr) {
$this->x509 = \openssl_csr_sign( $this->x509 = \openssl_csr_sign(
$csr, $csr,
@ -145,13 +151,11 @@ class Certificate
); );
if ($this->x509/* && $this->canSign() && $this->canEncrypt()*/) { if ($this->x509/* && $this->canSign() && $this->canEncrypt()*/) {
$this->pkey = $pkey; $this->pkey = $pkey;
$privatekey = ''; $privateKey || \openssl_pkey_export($pkey, $privateKey, $passphrase);
$certificate = ''; $certificate = '';
$csrStr = '';
\openssl_pkey_export($pkey, $privatekey, $passphrase);
\openssl_x509_export($this->x509, $certificate); \openssl_x509_export($this->x509, $certificate);
return array( return array(
'pkey' => $privatekey, 'pkey' => $privateKey,
'x509' => $certificate, 'x509' => $certificate,
// 'pkcs12' => $this->asPKCS12($pkey, $passphrase/*, array $args = array()*/) // 'pkcs12' => $this->asPKCS12($pkey, $passphrase/*, array $args = array()*/)
// 'canSign' => $this->canSign(), // 'canSign' => $this->canSign(),
@ -167,7 +171,11 @@ class Certificate
} }
// returns binary data // returns binary data
public function asPKCS12(string $pass = '', array $args = array()) : string public function asPKCS12(
#[\SensitiveParameter]
string $pass = '',
array $args = array()
) : string
{ {
$out = ''; $out = '';
\openssl_pkcs12_export($this->x509, $out, $this->pkey, $pass, $args); \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)); $data = \openssl_x509_parse(\openssl_x509_read($certificate));
if (!$data) { if (!$data) {
\error_log("OpenSSL parse: " . \openssl_error_string()); \SnappyMail\Log::error('OpenSSL', "parse: " . \openssl_error_string());
return false; return false;
} }
$key = \str_replace(':', '', $data['extensions']['subjectKeyIdentifier'] ?? $data['hash']); $key = \str_replace(':', '', $data['extensions']['subjectKeyIdentifier'] ?? $data['hash']);
$filename = "{$this->homedir}/{$key}.crt"; $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); \file_put_contents("{$this->homedir}/{$key}.crt", $certificate);
// \unlink("{$this->homedir}/certificates.json"); // \unlink("{$this->homedir}/certificates.json");
$this->certificates(true); $this->certificates(true);
\SnappyMail\Log::debug('OpenSSL', "certificate {$key} imported");
} }
return true; return true;
} }
@ -50,7 +53,7 @@ class OpenSSL
public function certificates(bool $force = false) : array public function certificates(bool $force = false) : array
{ {
$cacheFile = "{$this->homedir}/certificates.json"; $cacheFile = "{$this->homedir}/certificates.json";
$result = \file_exists($cacheFile) $result = (!$force && \file_exists($cacheFile))
? \json_decode(\file_get_contents($cacheFile), true) ? \json_decode(\file_get_contents($cacheFile), true)
: null; : null;
if (!\is_array($result)) { if (!\is_array($result)) {
@ -98,7 +101,7 @@ class OpenSSL
} }
$result[] = $short; $result[] = $short;
} else { } 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)); \file_put_contents($cacheFile, \json_encode($result));
@ -129,8 +132,7 @@ class OpenSSL
} }
public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey, public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey,
#[\SensitiveParameter] ?\SnappyMail\SensitiveString $passphrase = null
?string $passphrase = null
) : void ) : void
{ {
$this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase); $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 public function decrypt(/*string|Temporary*/ $input) : ?string
{ {
if (\is_string($input)) { if (\is_string($input)) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Signature valide pour %USER%", "GOOD_SIGNATURE": "Signature valide pour %USER%",
"ERROR": "Erreur %TYPE%: %ERROR%", "ERROR": "Erreur %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed", "CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until" "VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
}, },
"OPENPGP": { "OPENPGP": {
"POPUP_IMPORT_TITLE": "Importer la clef OpenPGP", "POPUP_IMPORT_TITLE": "Importer la clef OpenPGP",
@ -303,6 +304,7 @@
}, },
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate", "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates", "CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Message signé par S\/MIME", "SIGNED_MESSAGE": "Message signé par S\/MIME",
"ENCRYPTED_MESSAGE": "Message chiffré 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%", "GOOD_SIGNATURE": "Jó aláírás tőle: %USER%",
"ERROR": "%TYPE% hiba: %ERROR%", "ERROR": "%TYPE% hiba: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed", "CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until" "VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
}, },
"OPENPGP": { "OPENPGP": {
"POPUP_IMPORT_TITLE": "OpenPGP kulcs importálás", "POPUP_IMPORT_TITLE": "OpenPGP kulcs importálás",
@ -303,6 +304,7 @@
}, },
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate", "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates", "CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME aláírt üzenet", "SIGNED_MESSAGE": "S\/MIME aláírt üzenet",
"ENCRYPTED_MESSAGE": "S\/MIME kódolt üzenet", "ENCRYPTED_MESSAGE": "S\/MIME kódolt üzenet",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -282,7 +282,8 @@
"GOOD_SIGNATURE": "Dober podpis od %USER%", "GOOD_SIGNATURE": "Dober podpis od %USER%",
"ERROR": "%TYPE% napaka: %ERROR%", "ERROR": "%TYPE% napaka: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed", "CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until" "VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
}, },
"OPENPGP": { "OPENPGP": {
"POPUP_IMPORT_TITLE": "Uvoz OpenPGP ključa", "POPUP_IMPORT_TITLE": "Uvoz OpenPGP ključa",
@ -303,6 +304,7 @@
}, },
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate", "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates", "CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Sporočilo, podpisano z S\/MIME", "SIGNED_MESSAGE": "Sporočilo, podpisano z S\/MIME",
"ENCRYPTED_MESSAGE": "Sporočilo, šifrirano 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%", "GOOD_SIGNATURE": "Godkänd signatur för %USER%",
"ERROR": "%TYPE%-fel: %ERROR%", "ERROR": "%TYPE%-fel: %ERROR%",
"CREATE_SELF_SIGNED": "Create self-signed", "CREATE_SELF_SIGNED": "Create self-signed",
"VALID_UNTIL": "Valid until" "VALID_UNTIL": "Valid until",
"PRIVATE_KEY": "Private key"
}, },
"OPENPGP": { "OPENPGP": {
"POPUP_IMPORT_TITLE": "Import av OpenPGP-nyckel", "POPUP_IMPORT_TITLE": "Import av OpenPGP-nyckel",
@ -303,6 +304,7 @@
}, },
"SMIME": { "SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate", "POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATE": "Certificate",
"CERTIFICATES": "S\/MIME Certificates", "CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signerat meddelande", "SIGNED_MESSAGE": "S\/MIME-signerat meddelande",
"ENCRYPTED_MESSAGE": "S\/MIME-krypterat meddelande", "ENCRYPTED_MESSAGE": "S\/MIME-krypterat meddelande",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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