summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
authorAndrew Dolgov <fox@fakecake.org>2025-04-07 20:08:17 +0300
committerAndrew Dolgov <fox@fakecake.org>2025-04-07 20:09:31 +0300
commit026d68fc2d0f24e4f2d46c5743a22f42053caa67 (patch)
treef4552b9d8090bca446bdf0e203b4a76a53a58ebf /classes
parentbb2c4b380165731c3f8abf0596fffb2a0953265b (diff)
add optional encryption for stored session data using Sodium library
Diffstat (limited to 'classes')
-rw-r--r--classes/Config.php53
-rw-r--r--classes/Sessions.php17
2 files changed, 68 insertions, 2 deletions
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=?');