summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
authorAndrew Dolgov <fox@fakecake.org>2025-04-08 08:55:44 +0300
committerAndrew Dolgov <fox@fakecake.org>2025-04-08 08:55:44 +0300
commit25d3ce4ee8f411a19c3a0e69ebb5c575c16243a8 (patch)
tree81175cf9f11cdf6bf87dfc78f5d1f430d92297a9 /classes
parent58677fc791604bd891fb1ef4f4cc5e040ce8e39f (diff)
drop SESSION-specific stuff and move encrypt/decrypt helpers to a separate class; add a command line flag to generate encryption keys
Diffstat (limited to 'classes')
-rw-r--r--classes/Config.php6
-rw-r--r--classes/Crypt.php62
-rw-r--r--classes/Sessions.php54
3 files changed, 70 insertions, 52 deletions
diff --git a/classes/Config.php b/classes/Config.php
index c9acad93e..e906419dc 100644
--- a/classes/Config.php
+++ b/classes/Config.php
@@ -192,8 +192,8 @@ 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 key to transparently encrypt stored session data using Sodium library (XChaCha20-Poly1305) - generate using bin2hex(sodium_crypto_aead_xchacha20poly1305_ietf_keygen()) */
- const SESSION_ENCRYPTION_KEY = "SESSION_ENCRYPTION_KEY";
+ /** optional key to transparently encrypt sensitive data (currently limited to sessions); key is a 32 byte hex string may be generated using update.php --gen-encryption-key */
+ const ENCRYPTION_KEY = "ENCRYPTION_KEY";
/** default values for all global configuration options */
private const _DEFAULTS = [
@@ -253,7 +253,7 @@ class Config {
Config::T_STRING ],
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
Config::DISABLE_LOGIN_FORM => [ "", Config::T_BOOL ],
- Config::SESSION_ENCRYPTION_KEY => [ "", Config::T_STRING ]
+ Config::ENCRYPTION_KEY => [ "", Config::T_STRING ]
];
private static ?Config $instance = null;
diff --git a/classes/Crypt.php b/classes/Crypt.php
new file mode 100644
index 000000000..d832e6530
--- /dev/null
+++ b/classes/Crypt.php
@@ -0,0 +1,62 @@
+<?php
+class Crypt {
+
+ /** the only algo supported at the moment */
+ private const ENCRYPT_ALGO = 'xchacha20poly1305_ietf';
+
+ /** currently only generates keys using sodium_crypto_aead_chacha20poly1305_keygen() i.e. one supported Crypt::ENCRYPT_ALGO
+ * @return string random 256-bit (for ChaCha20-Poly1305) binary string
+ */
+ static function generate_key() : string {
+ return sodium_crypto_aead_chacha20poly1305_keygen();
+ }
+
+ /** encrypts provided ciphertext using Config::ENCRYPTION_KEY into an encrypted object
+ *
+ * @return array{'algo': string, 'nonce': string, 'payload': string} encrypted data object containing algo, nonce, and encrypted data
+ */
+ static function encrypt_string(string $ciphertext) : array {
+ $key = Config::get(Config::ENCRYPTION_KEY);
+
+ if (!$key)
+ throw new Exception("Crypt::encrypt_string() failed to encrypt - key is not available");
+
+ $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::ENCRYPT_ALGO,
+ 'nonce' => $nonce,
+ 'payload' => $payload,
+ ];
+
+ return $encrypted_data;
+ }
+
+ throw new Exception("Crypt::encrypt_string() failed to encrypt ciphertext");
+ }
+
+ /** decrypts payload of a valid encrypted object using Config::ENCRYPTION_KEY
+ *
+ * @param array{'algo': string, 'nonce': string, 'payload': string} $encrypted_data
+ *
+ * @return string decrypted string payload
+ */
+ static function decrypt_string(array $encrypted_data) : string {
+ $key = Config::get(Config::ENCRYPTION_KEY);
+
+ if (!$key)
+ throw new Exception("Crypt::decrypt_string() failed to decrypt - key is not available");
+
+ // only one is supported for the time being
+ switch ($encrypted_data['algo']) {
+ case self::ENCRYPT_ALGO:
+ return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_data['payload'], '', $encrypted_data['nonce'], hex2bin($key));
+ }
+
+ throw new Exception('Crypt::decrypt_string() failed to decrypt passed encrypted data object, unsupported algo: ' . $encrypted_data['algo']);
+ }
+
+} \ No newline at end of file
diff --git a/classes/Sessions.php b/classes/Sessions.php
index d8f14eed0..f3c0cea42 100644
--- a/classes/Sessions.php
+++ b/classes/Sessions.php
@@ -8,8 +8,6 @@ class Sessions implements \SessionHandlerInterface {
private int $session_expire;
private string $session_name;
- private const SODIUM_ALGO = 'xchacha20poly1305_ietf';
-
public function __construct() {
$this->session_expire = min(2147483647 - time() - 1, Config::get(Config::SESSION_COOKIE_LIFETIME));
$this->session_name = Config::get(Config::SESSION_NAME);
@@ -55,48 +53,6 @@ class Sessions implements \SessionHandlerInterface {
return true;
}
- /** encrypts provided ciphertext using Sodium symmetric encryption key if available via Config::SESSION_ENCRYPTION_KEY
- *
- * @return array<string,mixed> encrypted data object containing algo, nonce, and encrypted data
- *
- */
- private function encrypt_string(string $ciphertext) : array {
- $key = Config::get(Config::SESSION_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::SESSION_ENCRYPTION_KEY is available and object is in correct format
- *
- * @param array<string,mixed> $encrypted_data
- *
- * @return string decrypted string payload
- */
- private function decrypt_string(array $encrypted_data) : string {
- $key = Config::get(Config::SESSION_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');
- }
-
public function read(string $id): false|string {
$sth = Db::pdo()->prepare('SELECT data FROM ttrss_sessions WHERE id=?');
$sth->execute([$id]);
@@ -104,14 +60,14 @@ class Sessions implements \SessionHandlerInterface {
if ($row = $sth->fetch()) {
$data = base64_decode($row['data']);
- if (Config::get(Config::SESSION_ENCRYPTION_KEY)) {
+ if (Config::get(Config::ENCRYPTION_KEY)) {
$unserialized_data = @unserialize($data); // avoid leaking plaintext session via error message
if ($unserialized_data !== false)
- return $this->decrypt_string($unserialized_data);
+ return Crypt::decrypt_string($unserialized_data);
}
- // if Sodium key is missing or session data is not in serialized format, return as-is
+ // if encryption key is missing or session data is not in serialized format, assume plaintext data and return as-is
return $data;
}
@@ -124,8 +80,8 @@ class Sessions implements \SessionHandlerInterface {
public function write(string $id, string $data): bool {
- if (Config::get(Config::SESSION_ENCRYPTION_KEY))
- $data = serialize($this->encrypt_string($data));
+ if (Config::get(Config::ENCRYPTION_KEY))
+ $data = serialize(Crypt::encrypt_string($data));
$data = base64_encode($data);