Skip to content

Commit 3190e44

Browse files
Merge branch 'encryptCredentials-850' into 'main'
Adiciona criptografia das credenciais See merge request softwares-pkp/plugins_ojs/doiscielo!30
2 parents 8dbd546 + 2db3320 commit 3190e44

13 files changed

Lines changed: 217 additions & 11 deletions

.gitlab-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include:
1515

1616
.integration_tests_template:
1717
before_script:
18+
- sed -i 's/api_key_secret = ""/api_key_secret = "$API_KEY_SECRET"/' /var/www/$APPLICATION/config.inc.php
1819
- apt update && apt install poppler-utils -yqq
1920

2021
ops_integration_tests:

ScieloScreeningPlugin.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
use APP\template\TemplateManager;
2323
use PKP\core\JSONMessage;
2424
use APP\pages\submission\SubmissionHandler;
25+
use Illuminate\Database\Migrations\Migration;
2526
use APP\plugins\generic\scieloScreening\classes\components\forms\NumberContributorsForm;
2627
use APP\plugins\generic\scieloScreening\classes\ScreeningExecutor;
2728
use APP\plugins\generic\scieloScreening\classes\ScreeningChecker;
2829
use APP\plugins\generic\scieloScreening\classes\DocumentChecker;
2930
use APP\plugins\generic\scieloScreening\classes\OrcidClient;
31+
use APP\plugins\generic\scieloScreening\classes\migration\EncryptLegacyCredentials;
3032
use APP\plugins\generic\scieloScreening\ScieloScreeningSettingsForm;
3133

3234
class ScieloScreeningPlugin extends GenericPlugin
@@ -66,6 +68,11 @@ public function getDescription()
6668
return __('plugins.generic.scieloScreening.description');
6769
}
6870

71+
public function getInstallMigration(): Migration
72+
{
73+
return new EncryptLegacyCredentials();
74+
}
75+
6976
public function getActions($request, $actionArgs)
7077
{
7178
$router = $request->getRouter();

ScieloScreeningSettingsForm.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PKP\form\validation\FormValidatorPost;
2424
use PKP\form\validation\FormValidatorCSRF;
2525
use PKP\form\validation\FormValidatorCustom;
26+
use APP\plugins\generic\scieloScreening\classes\APIKeyEncryption;
2627

2728
class ScieloScreeningSettingsForm extends Form
2829
{
@@ -31,15 +32,19 @@ class ScieloScreeningSettingsForm extends Form
3132
'orcidClientId' => 'string',
3233
'orcidClientSecret' => 'string',
3334
);
34-
3535
public $contextId;
3636
public $plugin;
37+
private $encrypter;
3738

3839
public function __construct($plugin, $contextId)
3940
{
4041
$this->contextId = $contextId;
4142
$this->plugin = $plugin;
42-
parent::__construct($plugin->getTemplateResource('settingsForm.tpl'));
43+
$this->encrypter = new APIKeyEncryption();
44+
45+
$template = $this->encrypter->secretConfigExists() ? 'settingsForm.tpl' : 'settingsFormEmptySecret.tpl';
46+
parent::__construct($plugin->getTemplateResource($template));
47+
4348
$this->addCheck(new FormValidatorPost($this));
4449
$this->addCheck(new FormValidatorCSRF($this));
4550

@@ -59,8 +64,17 @@ public function initData()
5964
$contextId = $this->contextId;
6065
$plugin = &$this->plugin;
6166
$this->_data = array();
67+
6268
foreach (self::CONFIG_VARS as $configVar => $type) {
63-
$this->_data[$configVar] = $plugin->getSetting($contextId, $configVar);
69+
$settingValue = $plugin->getSetting($contextId, $configVar);
70+
71+
if ($configVar == 'orcidClientId' || $configVar == 'orcidClientSecret') {
72+
if (!empty($settingValue) && $this->encrypter->textIsEncrypted($settingValue)) {
73+
$settingValue = $this->encrypter->decryptString($settingValue);
74+
}
75+
}
76+
77+
$this->_data[$configVar] = $settingValue;
6478
}
6579
}
6680

@@ -86,7 +100,8 @@ public function execute(...$functionArgs)
86100
if ($configVar === 'orcidAPIPath') {
87101
$plugin->updateSetting($contextId, $configVar, trim($this->getData($configVar), "\"\';"), $type);
88102
} else {
89-
$plugin->updateSetting($contextId, $configVar, $this->getData($configVar), $type);
103+
$encryptedValue = $this->encrypter->encryptString($this->getData($configVar));
104+
$plugin->updateSetting($contextId, $configVar, $encryptedValue, $type);
90105
}
91106
}
92107

classes/APIKeyEncryption.php

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\scieloScreening\classes;
4+
5+
use Exception;
6+
use Illuminate\Encryption\Encrypter;
7+
use PKP\config\Config;
8+
9+
class APIKeyEncryption
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("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("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("Failed to decrypt string");
79+
}
80+
81+
return $decryptedString;
82+
}
83+
}

classes/OrcidClient.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace APP\plugins\generic\scieloScreening\classes;
44

55
use APP\core\Application;
6+
use APP\plugins\generic\scieloScreening\classes\APIKeyEncryption;
67

78
class OrcidClient
89
{
@@ -24,15 +25,28 @@ public function __construct($plugin, $contextId)
2425
$this->contextId = $contextId;
2526
}
2627

28+
private function getPluginSetting($settingName)
29+
{
30+
$settingValue = $this->plugin->getSetting($this->contextId, $settingName);
31+
if ($settingName == 'orcidClientId' || $settingName == 'orcidClientSecret') {
32+
$encrypter = new APIKeyEncryption();
33+
if (!empty($settingValue) && $encrypter->textIsEncrypted($settingValue)) {
34+
$settingValue = $encrypter->decryptString($settingValue);
35+
}
36+
}
37+
38+
return $settingValue;
39+
}
40+
2741
public function getReadPublicAccessToken(): string
2842
{
2943
$httpClient = Application::get()->getHttpClient();
3044

31-
$tokenUrl = $this->plugin->getSetting($this->contextId, 'orcidAPIPath') . 'oauth/token';
45+
$tokenUrl = $this->getPluginSetting('orcidAPIPath') . 'oauth/token';
3246
$requestHeaders = ['Accept' => 'application/json'];
3347
$requestData = [
34-
'client_id' => $this->plugin->getSetting($this->contextId, 'orcidClientId'),
35-
'client_secret' => $this->plugin->getSetting($this->contextId, 'orcidClientSecret'),
48+
'client_id' => $this->getPluginSetting('orcidClientId'),
49+
'client_secret' => $this->getPluginSetting('orcidClientSecret'),
3650
'grant_type' => 'client_credentials',
3751
'scope' => '/read-public'
3852
];
@@ -54,7 +68,7 @@ public function getOrcidWorks(string $orcid, string $accessToken): array
5468
{
5569
$httpClient = Application::get()->getHttpClient();
5670

57-
$worksUrl = $this->plugin->getSetting($this->contextId, 'orcidAPIPath') . 'v3.0/' . urlencode($orcid) . '/works';
71+
$worksUrl = $this->getPluginSetting('orcidAPIPath') . 'v3.0/' . urlencode($orcid) . '/works';
5872
$response = $httpClient->request(
5973
'GET',
6074
$worksUrl,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace APP\plugins\generic\scieloScreening\classes\migration;
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Support\Facades\DB;
7+
use PKP\install\DowngradeNotSupportedException;
8+
use APP\plugins\generic\scieloScreening\classes\APIKeyEncryption;
9+
10+
class EncryptLegacyCredentials extends Migration
11+
{
12+
private const PLUGIN_NAME_SETTINGS = 'scieloscreeningplugin';
13+
private const PLUGIN_CREDENTIALS_SETTINGS = [
14+
'orcidClientId',
15+
'orcidClientSecret'
16+
];
17+
18+
public function up(): void
19+
{
20+
$credentialSettings = $this->getCredentialSettings();
21+
22+
if (!empty($credentialSettings)) {
23+
$encrypter = new APIKeyEncryption();
24+
25+
foreach ($credentialSettings as $credentialSetting) {
26+
$credentialSetting = get_object_vars($credentialSetting);
27+
28+
if ($encrypter->textIsEncrypted($credentialSetting['setting_value'])) {
29+
continue;
30+
}
31+
$this->encryptCredential($credentialSetting);
32+
}
33+
}
34+
}
35+
36+
public function down(): void
37+
{
38+
throw new DowngradeNotSupportedException();
39+
}
40+
41+
private function getCredentialSettings()
42+
{
43+
return DB::table('plugin_settings')
44+
->where('plugin_name', self::PLUGIN_NAME_SETTINGS)
45+
->whereIn('setting_name', self::PLUGIN_CREDENTIALS_SETTINGS)
46+
->get();
47+
}
48+
49+
private function encryptCredential($credentialSetting)
50+
{
51+
$encrypter = new APIKeyEncryption();
52+
$encryptedSettingValue = $encrypter->encryptString($credentialSetting['setting_value']);
53+
54+
DB::table('plugin_settings')
55+
->where('context_id', $credentialSetting['context_id'])
56+
->where('plugin_name', self::PLUGIN_NAME_SETTINGS)
57+
->where('setting_name', $credentialSetting['setting_name'])
58+
->update(['setting_value' => $encryptedSettingValue]);
59+
}
60+
}

cypress/tests/Test1_submissionWizard.cy.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ function addContributor(contributorData, toUpperCase = false) {
2525
cy.get('select[name="country"]').select(contributorData.country);
2626

2727
if ('orcid' in contributorData) {
28-
cy.get('.pkpFormField__control_top > .pkpButton').contains("Override").click();
2928
cy.get('input[name="orcid"]').type(contributorData.orcid, {delay: 0});
3029
}
3130

locale/en/locale.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ msgstr "ORCID API Settings"
157157
msgid "plugins.generic.scieloScreening.settings.description"
158158
msgstr "Please configure the ORCID API access for use in pulling ORCID records information"
159159

160+
msgid "plugins.generic.scieloScreening.settings.emptyApiSecretKey"
161+
msgstr "The administrator must set a secret in the site configuration file ('api_key_secret')."
162+
160163
msgid "plugins.generic.scieloScreening.settings.globallyconfigured"
161164
msgstr "The ORCID API was configured globally by the host. The following credentials have been saved."
162165

locale/es/locale.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ msgstr "Configuración de la API de ORCID"
157157
msgid "plugins.generic.scieloScreening.settings.description"
158158
msgstr "Por favor configure el acceso a la API de ORCID para extraer información del registro de ORCID."
159159

160+
msgid "plugins.generic.scieloScreening.settings.emptyApiSecretKey"
161+
msgstr "Es necesario que el administrador establezca un secreto en el archivo de configuración del sitio ('api_key_secret')."
162+
160163
msgid "plugins.generic.scieloScreening.settings.globallyconfigured"
161164
msgstr "El host configuró globalmente la API de ORCID. Se han guardado las siguientes credenciales."
162165

locale/pt_BR/locale.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ msgstr "Configurações da API ORCID"
156156
msgid "plugins.generic.scieloScreening.settings.description"
157157
msgstr "Por favor, configure o acesso a API do ORCID para recuperar informações de registros ORCID"
158158

159+
msgid "plugins.generic.scieloScreening.settings.emptyApiSecretKey"
160+
msgstr "É necessário que o administrador defina um segredo no arquivo de configuração do site ('api_key_secret')."
161+
159162
msgid "plugins.generic.scieloScreening.settings.globallyconfigured"
160163
msgstr "A API ORCID foi configurada globalmente pelo host. As seguintes credenciais foram salvas."
161164

0 commit comments

Comments
 (0)