add optional encryption for stored session data using Sodium library
This commit is contained in:
@@ -16,7 +16,7 @@ RUN [ ! -z ${ALPINE_MIRROR} ] && \
|
|||||||
apk add --no-cache ca-certificates dcron git postgresql-client rsync sudo tzdata \
|
apk add --no-cache ca-certificates dcron git postgresql-client rsync sudo tzdata \
|
||||||
php${PHP_SUFFIX} \
|
php${PHP_SUFFIX} \
|
||||||
$(for p in ctype curl dom exif fileinfo fpm gd iconv intl json mbstring opcache \
|
$(for p in ctype curl dom exif fileinfo fpm gd iconv intl json mbstring opcache \
|
||||||
openssl pcntl pdo pdo_pgsql pecl-apcu pecl-xdebug phar posix session simplexml sockets tokenizer xml xmlwriter zip; do \
|
openssl pcntl pdo pdo_pgsql pecl-apcu pecl-xdebug phar posix session simplexml sockets sodium tokenizer xml xmlwriter zip; do \
|
||||||
php_pkgs="$php_pkgs php${PHP_SUFFIX}-$p"; \
|
php_pkgs="$php_pkgs php${PHP_SUFFIX}-$p"; \
|
||||||
done; \
|
done; \
|
||||||
echo $php_pkgs) && \
|
echo $php_pkgs) && \
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class Config {
|
|||||||
|
|
||||||
const SCHEMA_VERSION = 147;
|
const SCHEMA_VERSION = 147;
|
||||||
|
|
||||||
|
const SODIUM_ALGO = 'xchacha20poly1305_ietf';
|
||||||
|
|
||||||
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
|
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
|
||||||
*
|
*
|
||||||
* DB_TYPE becomes:
|
* DB_TYPE becomes:
|
||||||
@@ -192,6 +194,12 @@ class Config {
|
|||||||
/** disables login form controls except HOOK_LOGINFORM_ADDITIONAL_BUTTONS (for SSO providers), also prevents logging in through auth_internal */
|
/** disables login form controls except HOOK_LOGINFORM_ADDITIONAL_BUTTONS (for SSO providers), also prevents logging in through auth_internal */
|
||||||
const DISABLE_LOGIN_FORM = "DISABLE_LOGIN_FORM";
|
const DISABLE_LOGIN_FORM = "DISABLE_LOGIN_FORM";
|
||||||
|
|
||||||
|
/** optional symmetric encryption key for Sodium library (XChaCha20-Poly1305) - generate using bin2hex(sodium_crypto_aead_xchacha20poly1305_ietf_keygen())
|
||||||
|
*
|
||||||
|
* if set, used to transparently encrypt stored session data in the database
|
||||||
|
*/
|
||||||
|
const SODIUM_ENCRYPTION_KEY = "SODIUM_ENCRYPTION_KEY";
|
||||||
|
|
||||||
/** default values for all global configuration options */
|
/** default values for all global configuration options */
|
||||||
private const _DEFAULTS = [
|
private const _DEFAULTS = [
|
||||||
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
|
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
|
||||||
@@ -249,7 +257,8 @@ class Config {
|
|||||||
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
|
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
|
||||||
Config::T_STRING ],
|
Config::T_STRING ],
|
||||||
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
|
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
|
||||||
Config::DISABLE_LOGIN_FORM => [ "", Config::T_BOOL ]
|
Config::DISABLE_LOGIN_FORM => [ "", Config::T_BOOL ],
|
||||||
|
Config::SODIUM_ENCRYPTION_KEY => [ "", Config::T_STRING ]
|
||||||
];
|
];
|
||||||
|
|
||||||
private static ?Config $instance = null;
|
private static ?Config $instance = null;
|
||||||
@@ -298,6 +307,48 @@ class Config {
|
|||||||
return self::get_instance()->_get_version($as_string);
|
return self::get_instance()->_get_version($as_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** encrypts provided ciphertext using Sodium symmetric encryption key if available via Config::SODIUM_ENCRYPTION_KEY
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|false encrypted data object containing algo, nonce, and encrypted data or false if encryption failed
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static function encrypt_string(string $ciphertext) : array|false {
|
||||||
|
$key = Config::get(Config::SODIUM_ENCRYPTION_KEY);
|
||||||
|
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||||
|
|
||||||
|
$payload = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($ciphertext, '', $nonce, hex2bin($key));
|
||||||
|
|
||||||
|
if ($payload) {
|
||||||
|
$encrypted_data = [
|
||||||
|
'algo' => self::SODIUM_ALGO,
|
||||||
|
'nonce' => $nonce,
|
||||||
|
'payload' => $payload,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $encrypted_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Config::encrypt_string() failed to encrypt ciphertext");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** decrypts payload of encrypted object if Config::SODIUM_ENCRYPTION_KEY is available and object is in correct format
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $encrypted_data
|
||||||
|
*
|
||||||
|
* @return string|false decrypted string payload or false if decryption failed
|
||||||
|
*/
|
||||||
|
static function decrypt_string(array $encrypted_data) : string|false {
|
||||||
|
$key = Config::get(Config::SODIUM_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
if ($encrypted_data['algo'] === self::SODIUM_ALGO) {
|
||||||
|
$payload = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_data['payload'], '', $encrypted_data['nonce'], hex2bin($key));
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Config::decrypt_string() failed to decrypt passed encrypted data');
|
||||||
|
}
|
||||||
|
|
||||||
// returns version showing (if possible) full timestamp of commit id
|
// returns version showing (if possible) full timestamp of commit id
|
||||||
static function get_version_html() : string {
|
static function get_version_html() : string {
|
||||||
$version = self::get_version(false);
|
$version = self::get_version(false);
|
||||||
|
|||||||
@@ -58,7 +58,17 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
return base64_decode($row['data']);
|
$data = base64_decode($row['data']);
|
||||||
|
|
||||||
|
if (Config::get(Config::SODIUM_ENCRYPTION_KEY)) {
|
||||||
|
$unserialized_data = unserialize($data);
|
||||||
|
|
||||||
|
if ($unserialized_data !== false)
|
||||||
|
return Config::decrypt_string($unserialized_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if Sodium key is missing or session data is not in serialized format, return as-is
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
$expire = time() + $this->session_expire;
|
$expire = time() + $this->session_expire;
|
||||||
@@ -69,7 +79,12 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function write(string $id, string $data): bool {
|
public function write(string $id, string $data): bool {
|
||||||
|
|
||||||
|
if (Config::get(Config::SODIUM_ENCRYPTION_KEY))
|
||||||
|
$data = serialize(Config::encrypt_string($data));
|
||||||
|
|
||||||
$data = base64_encode($data);
|
$data = base64_encode($data);
|
||||||
|
|
||||||
$expire = time() + $this->session_expire;
|
$expire = time() + $this->session_expire;
|
||||||
|
|
||||||
$sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?');
|
$sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?');
|
||||||
|
|||||||
Reference in New Issue
Block a user