Skip to content

Commit 48b2e4c

Browse files
Merge pull request #52 from lepidus/stable-3_3_0
fix/password encryption (OMP 3.3.0)
2 parents 5ed3fdb + b6a359a commit 48b2e4c

9 files changed

Lines changed: 190 additions & 19 deletions

File tree

.gitlab-ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ include:
1010

1111
.unit_test_template:
1212
before_script:
13-
- rm -rf lib/APIKeyEncryption
14-
- git submodule update --init --depth 1
1513
- composer install
1614

1715
plugin_unit_tests_omp:

.gitmodules

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

ThothSettingsForm.inc.php

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use ThothApi\GraphQL\Client;
2020

2121
import('lib.pkp.classes.form.Form');
22-
import('plugins.generic.thoth.lib.APIKeyEncryption.APIKeyEncryption');
22+
import('plugins.generic.thoth.classes.encryption.DataEncryption');
2323

2424
class ThothSettingsForm extends Form
2525
{
@@ -38,7 +38,8 @@ public function __construct($plugin, $contextId)
3838
$this->contextId = $contextId;
3939
$this->plugin = $plugin;
4040

41-
$template = APIKeyEncryption::secretConfigExists() ? 'settingsForm.tpl' : 'tokenError.tpl';
41+
$encryption = new DataEncryption();
42+
$template = $encryption->secretConfigExists() ? 'settingsForm.tpl' : 'tokenError.tpl';
4243
parent::__construct($plugin->getTemplateResource($template));
4344

4445
$form = $this;
@@ -75,9 +76,10 @@ public function initData()
7576
{
7677
foreach (self::SETTINGS as $setting) {
7778
if ($setting == 'password') {
79+
$encryption = new DataEncryption();
7880
$password = $this->plugin->getSetting($this->contextId, $setting);
79-
$this->_data[$setting] = (APIKeyEncryption::secretConfigExists() && $password) ?
80-
APIKeyEncryption::decryptString($password) :
81+
$this->_data[$setting] = ($encryption->secretConfigExists() && $password) ?
82+
$encryption->decryptString($password) :
8183
null;
8284
continue;
8385
}
@@ -99,14 +101,21 @@ public function fetch($request, $template = null, $display = false)
99101

100102
public function execute(...$functionArgs)
101103
{
104+
$this->encryptPassword();
102105
foreach (self::SETTINGS as $setting) {
103-
if ($setting == 'password') {
104-
$encryptedPassword = APIKeyEncryption::encryptString(trim($this->getData($setting)));
105-
$this->plugin->updateSetting($this->contextId, $setting, $encryptedPassword, 'string');
106-
continue;
107-
}
108106
$this->plugin->updateSetting($this->contextId, $setting, trim($this->getData($setting)), 'string');
109107
}
110108
parent::execute(...$functionArgs);
111109
}
110+
111+
private function encryptPassword()
112+
{
113+
$encryption = new DataEncryption();
114+
$password = $this->getData('password');
115+
116+
if (!$encryption->textIsEncrypted($password)) {
117+
$encryptedPassword = $encryption->encryptString($password);
118+
$this->setData('password', $encryptedPassword);
119+
}
120+
}
112121
}

classes/container/providers/ThothRepositoryProvider.inc.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ThothApi\GraphQL\Client;
1919

2020
import('plugins.generic.thoth.classes.container.providers.ContainerProvider');
21+
import('plugins.generic.thoth.classes.encryption.DataEncryption');
2122
import('plugins.generic.thoth.classes.factories.ThothBookFactory');
2223
import('plugins.generic.thoth.classes.factories.ThothChapterFactory');
2324
import('plugins.generic.thoth.classes.factories.ThothContributionFactory');
@@ -39,13 +40,13 @@
3940
import('plugins.generic.thoth.classes.repositories.ThothSubjectRepository');
4041
import('plugins.generic.thoth.classes.repositories.ThothWorkRelationRepository');
4142
import('plugins.generic.thoth.classes.repositories.ThothWorkRepository');
42-
import('plugins.generic.thoth.lib.APIKeyEncryption.APIKeyEncryption');
4343

4444
class ThothRepositoryProvider implements ContainerProvider
4545
{
4646
public function register($container)
4747
{
4848
$container->set('config', function ($container) {
49+
$encryption = new DataEncryption();
4950
$pluginSettingsDao = & DAORegistry::getDAO('PluginSettingsDAO');
5051
$contextId = Application::get()->getRequest()->getContext()->getId();
5152

@@ -56,7 +57,7 @@ public function register($container)
5657
return [
5758
'testEnvironment' => $testEnvironment,
5859
'email' => $email,
59-
'password' => APIKeyEncryption::decryptString($password)
60+
'password' => $encryption->decryptString($password)
6061
];
6162
});
6263

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

lib/APIKeyEncryption

Lines changed: 0 additions & 1 deletion
This file was deleted.

upgrade.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE install SYSTEM "../../../lib/pkp/dtd/install.dtd">
3+
4+
<install version="0.1.11.0">
5+
<migration
6+
class="plugins.generic.thoth.classes.migrations.PasswordEncryptionMigration"
7+
/>
8+
</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.1.10.6</release>
7-
<date>2025-08-26</date>
6+
<release>0.1.11.0</release>
7+
<date>2025-10-30</date>
88
<lazy-load>1</lazy-load>
99
<class>ThothPlugin</class>
1010
</version>

0 commit comments

Comments
 (0)