diff options
| -rw-r--r-- | .docker/app/Dockerfile | 2 | ||||
| -rw-r--r-- | classes/Config.php | 53 | ||||
| -rw-r--r-- | classes/Sessions.php | 17 |
3 files changed, 69 insertions, 3 deletions
diff --git a/.docker/app/Dockerfile b/.docker/app/Dockerfile index acfc1a9e5..7a87f7ea3 100644 --- a/.docker/app/Dockerfile +++ b/.docker/app/Dockerfile @@ -16,7 +16,7 @@ RUN [ ! -z ${ALPINE_MIRROR} ] && \ apk add --no-cache ca-certificates dcron git postgresql-client rsync sudo tzdata \ php${PHP_SUFFIX} \ $(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"; \ done; \ echo $php_pkgs) && \ diff --git a/classes/Config.php b/classes/Config.php index 92037ff74..11ce5ccfa 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -8,6 +8,8 @@ class Config { const SCHEMA_VERSION = 147; + const SODIUM_ALGO = 'xchacha20poly1305_ietf'; + /** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX: * * 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 */ 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 */ private const _DEFAULTS = [ 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::T_STRING ], 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; @@ -298,6 +307,48 @@ class Config { 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 static function get_version_html() : string { $version = self::get_version(false); diff --git a/classes/Sessions.php b/classes/Sessions.php index 5c586154b..e8cba1765 100644 --- a/classes/Sessions.php +++ b/classes/Sessions.php @@ -58,7 +58,17 @@ class Sessions implements \SessionHandlerInterface { $sth->execute([$id]); 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; @@ -69,7 +79,12 @@ class Sessions implements \SessionHandlerInterface { } 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); + $expire = time() + $this->session_expire; $sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?'); |