Skip to content

Commit 02b9fbc

Browse files
committed
Merge branch 'password_encryption_3_4_0-1073' into 'main'
Fix password encryption See merge request softwares-pkp/plugins_ojs/thoth-omp-plugin!96
2 parents a676a79 + 3349d7d commit 02b9fbc

7 files changed

Lines changed: 193 additions & 61 deletions

File tree

ThothSettingsForm.inc.php

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* @brief Form for managers to modify Thoth plugin settings
1717
*/
1818

19-
use APP\plugins\generic\thoth\lib\APIKeyEncryption;
19+
use APP\plugins\generic\thoth\classes\encryption\DataEncryption;
2020
use PKP\form\Form;
2121
use PKP\form\validation\FormValidatorCSRF;
2222
use PKP\form\validation\FormValidatorCustom;
@@ -30,6 +30,8 @@ class ThothSettingsForm extends Form
3030

3131
private $plugin;
3232

33+
private $encryption;
34+
3335
private const SETTINGS = [
3436
'email',
3537
'password',
@@ -40,8 +42,9 @@ public function __construct($plugin, $contextId)
4042
{
4143
$this->contextId = $contextId;
4244
$this->plugin = $plugin;
45+
$this->encryption = new DataEncryption();
4346

44-
$template = APIKeyEncryption::secretConfigExists() ? 'settingsForm.tpl' : 'tokenError.tpl';
47+
$template = $this->encryption->secretConfigExists() ? 'settingsForm.tpl' : 'tokenError.tpl';
4548
parent::__construct($plugin->getTemplateResource($template));
4649

4750
$form = $this;
@@ -78,8 +81,8 @@ public function initData()
7881
foreach (self::SETTINGS as $setting) {
7982
if ($setting == 'password') {
8083
$password = $this->plugin->getSetting($this->contextId, $setting);
81-
$this->_data[$setting] = (APIKeyEncryption::secretConfigExists() && $password) ?
82-
APIKeyEncryption::decryptString($password) :
84+
$this->_data[$setting] = ($this->encryption->secretConfigExists() && $password) ?
85+
$this->encryption->decryptString($password) :
8386
null;
8487
continue;
8588
}
@@ -101,14 +104,20 @@ public function fetch($request, $template = null, $display = false)
101104

102105
public function execute(...$functionArgs)
103106
{
107+
$this->encryptPassword();
104108
foreach (self::SETTINGS as $setting) {
105-
if ($setting == 'password') {
106-
$encryptedPassword = APIKeyEncryption::encryptString(trim($this->getData($setting)));
107-
$this->plugin->updateSetting($this->contextId, $setting, $encryptedPassword, 'string');
108-
continue;
109-
}
110109
$this->plugin->updateSetting($this->contextId, $setting, trim($this->getData($setting)), 'string');
111110
}
112111
parent::execute(...$functionArgs);
113112
}
113+
114+
private function encryptPassword()
115+
{
116+
$password = $this->getData('password');
117+
118+
if (!$this->encryption->textIsEncrypted($password)) {
119+
$encryptedPassword = $this->encryption->encryptString($password);
120+
$this->setData('password', $encryptedPassword);
121+
}
122+
}
114123
}

classes/container/providers/ThothRepositoryProvider.inc.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
require_once(__DIR__ . '/../../../vendor/autoload.php');
1818

1919
use APP\core\Application;
20-
use APP\plugins\generic\thoth\lib\APIKeyEncryption;
20+
use APP\plugins\generic\thoth\classes\encryption\DataEncryption;
2121
use PKP\db\DAORegistry;
2222
use ThothApi\GraphQL\Client;
2323

@@ -49,6 +49,7 @@ class ThothRepositoryProvider implements ContainerProvider
4949
public function register($container)
5050
{
5151
$container->set('config', function ($container) {
52+
$encryption = new DataEncryption();
5253
$pluginSettingsDao = & DAORegistry::getDAO('PluginSettingsDAO');
5354
$contextId = Application::get()->getRequest()->getContext()->getId();
5455

@@ -59,7 +60,7 @@ public function register($container)
5960
return [
6061
'testEnvironment' => $testEnvironment,
6162
'email' => $email,
62-
'password' => APIKeyEncryption::decryptString($password)
63+
'password' => $encryption->decryptString($password)
6364
];
6465
});
6566

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace APP\plugins\generic\thoth\classes\encryption;
4+
5+
use Exception;
6+
use Illuminate\Encryption\Encrypter;
7+
use PKP\config\Config;
8+
9+
class DataEncryption
10+
{
11+
private const ENCRYPTION_CIPHER = 'AES-256-CBC';
12+
private const BASE64_PREFIX = 'base64:';
13+
14+
public function secretConfigExists(): bool
15+
{
16+
try {
17+
$this->getSecretFromConfig();
18+
} catch (Exception $e) {
19+
return false;
20+
}
21+
return true;
22+
}
23+
24+
private function getSecretFromConfig(): string
25+
{
26+
$secret = Config::getVar('security', 'api_key_secret');
27+
if ($secret === "") {
28+
throw new Exception("Thoth Error: A secret must be set in the config file ('api_key_secret') so that keys can be encrypted and decrypted");
29+
}
30+
31+
return $this->normalizeSecret($secret);
32+
}
33+
34+
private function normalizeSecret(string $secret): string
35+
{
36+
return hash('sha256', $secret, true);
37+
}
38+
39+
public function textIsEncrypted(string $text): bool
40+
{
41+
if (!str_starts_with($text, self::BASE64_PREFIX)) {
42+
return false;
43+
}
44+
45+
try {
46+
$this->decryptString($text);
47+
return true;
48+
} catch (Exception $e) {
49+
return false;
50+
}
51+
}
52+
53+
public function encryptString(string $plainText): string
54+
{
55+
$secret = $this->getSecretFromConfig();
56+
$encrypter = new Encrypter($secret, self::ENCRYPTION_CIPHER);
57+
58+
try {
59+
$encryptedString = $encrypter->encrypt($plainText);
60+
} catch (Exception $e) {
61+
throw new Exception("Thoth Error: Failed to encrypt string");
62+
}
63+
64+
return self::BASE64_PREFIX . base64_encode($encryptedString);
65+
}
66+
67+
public function decryptString(string $encryptedText): string
68+
{
69+
$secret = $this->getSecretFromConfig();
70+
$encrypter = new Encrypter($secret, self::ENCRYPTION_CIPHER);
71+
72+
$encryptedText = str_replace(self::BASE64_PREFIX, '', $encryptedText);
73+
$payload = base64_decode($encryptedText);
74+
75+
try {
76+
$decryptedString = $encrypter->decrypt($payload);
77+
} catch (Exception $e) {
78+
throw new Exception("Thoth Error: Failed to decrypt string");
79+
}
80+
81+
return $decryptedString;
82+
}
83+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace APP\plugins\generic\thoth\classes\migrations;
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Support\Facades\DB;
7+
use APP\plugins\generic\thoth\classes\encryption\DataEncryption;
8+
9+
class PasswordEncryptionMigration extends Migration
10+
{
11+
public function up(): void
12+
{
13+
$encrypter = new DataEncryption();
14+
if (!$encrypter->secretConfigExists()) {
15+
return;
16+
}
17+
18+
DB::table('plugin_settings')
19+
->where('plugin_name', 'thothplugin')
20+
->where('setting_name', 'password')
21+
->get(['context_id', 'setting_value'])
22+
->each(function ($row) use ($encrypter) {
23+
if (empty($row->setting_value) || $encrypter->textIsEncrypted($row->setting_value)) {
24+
return;
25+
}
26+
27+
if ($this->isJWT($row->setting_value)) {
28+
$decodedPayload = $this->decodeJWT($row->setting_value);
29+
if ($decodedPayload !== null) {
30+
$row->setting_value = json_decode($decodedPayload);
31+
}
32+
}
33+
34+
$encryptedValue = $encrypter->encryptString($row->setting_value);
35+
DB::table('plugin_settings')
36+
->where('plugin_name', 'thothplugin')
37+
->where('context_id', $row->context_id)
38+
->where('setting_name', 'password')
39+
->update(['setting_value' => $encryptedValue]);
40+
});
41+
}
42+
43+
public function base64URLDecode($data)
44+
{
45+
$remainder = strlen($data) % 4;
46+
if ($remainder) {
47+
$padlen = 4 - $remainder;
48+
$data .= str_repeat('=', $padlen);
49+
}
50+
$data = strtr($data, '-_', '+/');
51+
return base64_decode($data);
52+
}
53+
54+
public function isJWT(string $token): bool
55+
{
56+
$parts = explode('.', $token);
57+
if (count($parts) !== 3) {
58+
return false;
59+
}
60+
61+
foreach ($parts as $part) {
62+
if (!preg_match('/^[A-Za-z0-9\-_]+$/', $part)) {
63+
return false;
64+
}
65+
}
66+
67+
[$header, $payload, $signature] = $parts;
68+
if ($this->base64URLDecode($header) === false || $this->base64URLDecode($payload) === false) {
69+
return false;
70+
}
71+
72+
return true;
73+
}
74+
75+
public function decodeJWT($string): ?string
76+
{
77+
list($header, $payload, $signature) = explode('.', $string);
78+
$decodedPayload = $this->base64URLDecode($payload);
79+
return $decodedPayload ? $decodedPayload : null;
80+
}
81+
}

lib/APIKeyEncryption.php

Lines changed: 0 additions & 48 deletions
This file was deleted.

upgrade.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE install SYSTEM "../../../lib/pkp/dtd/install.dtd">
3+
4+
<install version="0.2.7.0">
5+
<migration class="APP\plugins\generic\thoth\classes\migrations\PasswordEncryptionMigration" />
6+
</install>

version.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<version>
44
<application>thoth</application>
55
<type>plugins.generic</type>
6-
<release>0.2.8.6</release>
7-
<date>2025-08-26</date>
6+
<release>0.2.9.0</release>
7+
<date>2025-11-03</date>
88
<lazy-load>1</lazy-load>
99
<class>ThothPlugin</class>
1010
</version>

0 commit comments

Comments
 (0)