Skip to content

Commit d997d43

Browse files
authored
Merge pull request #585 from nextcloud/feat/sensitive-declarative-settings
feat(declarativeSettings): support sensitive Declarative settings values encryption
2 parents e8c4f12 + 819f104 commit d997d43

9 files changed

Lines changed: 221 additions & 18 deletions

File tree

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
3838
*Your insights, suggestions, and contributions are invaluable to us.*
3939
4040
]]></description>
41-
<version>32.0.0-dev.4</version>
41+
<version>32.0.0-dev.5</version>
4242
<licence>agpl</licence>
4343
<author mail="andrey18106x@gmail.com" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
4444
<author mail="bigcat88@icloud.com" homepage="https://github.com/bigcat88">Alexander Piskun</author>

lib/Controller/PreferencesController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ public function __construct(
4343
#[AppAPIAuth]
4444
#[PublicPage]
4545
#[NoCSRFRequired]
46-
public function setUserConfigValue(string $configKey, mixed $configValue): DataResponse {
46+
public function setUserConfigValue(string $configKey, mixed $configValue, ?int $sensitive = null): DataResponse {
4747
if ($configKey === '') {
4848
throw new OCSBadRequestException('Config key cannot be empty');
4949
}
5050
$userId = $this->userSession->getUser()->getUID();
5151
$appId = $this->request->getHeader('EX-APP-ID');
52-
$result = $this->exAppPreferenceService->setUserConfigValue($userId, $appId, $configKey, $configValue);
52+
$result = $this->exAppPreferenceService->setUserConfigValue($userId, $appId, $configKey, $configValue, $sensitive);
5353
if ($result instanceof ExAppPreference) {
5454
return new DataResponse($result, Http::STATUS_OK);
5555
}

lib/Db/ExAppPreference.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@
2121
* @method string getAppid()
2222
* @method string getConfigkey()
2323
* @method string getConfigvalue()
24+
* @method int getSensitive()
2425
* @method void setUserid(string $userid)
2526
* @method void setAppid(string $appid)
2627
* @method void setConfigkey(string $configkey)
2728
* @method void setConfigvalue(string $configvalue)
29+
* @method void setSensitive(int $sensitive)
2830
*/
2931
class ExAppPreference extends Entity implements JsonSerializable {
3032
protected $userid;
3133
protected $appid;
3234
protected $configkey;
3335
protected $configvalue;
36+
protected $sensitive;
3437

3538
/**
3639
* @param array $params
@@ -40,6 +43,7 @@ public function __construct(array $params = []) {
4043
$this->addType('appid', 'string');
4144
$this->addType('configkey', 'string');
4245
$this->addType('configvalue', 'string');
46+
$this->addType('sensitive', 'int');
4347

4448
if (isset($params['id'])) {
4549
$this->setId($params['id']);
@@ -56,6 +60,9 @@ public function __construct(array $params = []) {
5660
if (isset($params['configvalue'])) {
5761
$this->setConfigvalue($params['configvalue']);
5862
}
63+
if (isset($params['sensitive'])) {
64+
$this->setSensitive($params['sensitive']);
65+
}
5966
}
6067

6168
public function jsonSerialize(): array {
@@ -65,6 +72,7 @@ public function jsonSerialize(): array {
6572
'appid' => $this->getAppid(),
6673
'configkey' => $this->getConfigkey(),
6774
'configvalue' => $this->getConfigvalue(),
75+
'sensitive' => $this->getSensitive(),
6876
];
6977
}
7078
}

lib/Db/UI/SettingsForm.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ public function __construct(array $params = []) {
4747
}
4848
}
4949

50+
public function getSchemaField(string $fieldId): ?array {
51+
$scheme = $this->getScheme();
52+
foreach ($scheme['fields'] as $field) {
53+
if ($field['id'] === $fieldId) {
54+
return $field;
55+
}
56+
}
57+
return null;
58+
}
59+
5060
public function jsonSerialize(): array {
5161
return [
5262
'id' => $this->getId(),

lib/Listener/DeclarativeSettings/GetValueListener.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@
1414
use OCA\AppAPI\Service\UI\SettingsService;
1515
use OCP\EventDispatcher\Event;
1616
use OCP\EventDispatcher\IEventListener;
17+
use OCP\Security\ICrypto;
1718
use OCP\Settings\DeclarativeSettingsTypes;
1819
use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
20+
use Psr\Log\LoggerInterface;
1921

2022
/**
2123
* @template-implements IEventListener<Event>
2224
*/
2325
class GetValueListener implements IEventListener {
2426
public function __construct(
25-
private readonly SettingsService $service,
27+
private readonly SettingsService $service,
2628
private readonly ExAppPreferenceService $preferenceService,
27-
private readonly ExAppConfigService $configService,
29+
private readonly ExAppConfigService $configService,
30+
private readonly ICrypto $crypto,
31+
private readonly LoggerInterface $logger,
2832
) {
2933
}
3034

@@ -38,9 +42,20 @@ public function handle(Event $event): void {
3842
return;
3943
}
4044
$formSchema = $settingsForm->getScheme();
45+
$field = $settingsForm->getSchemaField($event->getFieldId());
46+
$isSensitive = isset($field['sensitive']) && $field['sensitive'] === true;
4147
if ($formSchema['section_type'] === DeclarativeSettingsTypes::SECTION_TYPE_ADMIN) {
4248
$existingValue = $this->configService->getAppConfig($event->getApp(), $event->getFieldId());
4349
if (!empty($existingValue)) {
50+
if ($isSensitive) {
51+
try {
52+
$decryptedValue = $this->crypto->decrypt($existingValue->getConfigvalue());
53+
$existingValue->setConfigvalue($decryptedValue);
54+
} catch (\Exception $e) {
55+
$this->logger->warning(sprintf('Failed to decrypt declarative setting for app %s, field %s', $event->getApp(), $event->getFieldId()), ['exception' => $e]);
56+
$existingValue->setConfigvalue('');
57+
}
58+
}
4459
$event->setValue($existingValue->getConfigvalue());
4560
return;
4661
}
@@ -51,6 +66,15 @@ public function handle(Event $event): void {
5166
[$event->getFieldId()],
5267
);
5368
if (!empty($existingValue)) {
69+
if ($isSensitive) {
70+
try {
71+
$decryptedValue = $this->crypto->decrypt($existingValue[0]['configvalue']);
72+
$existingValue[0]['configvalue'] = $decryptedValue;
73+
} catch (\Exception $e) {
74+
$this->logger->warning('Failed to decrypt declarative setting for app ' . $event->getApp() . ', field ' . $event->getFieldId(), ['exception' => $e]);
75+
$existingValue[0]['configvalue'] = '';
76+
}
77+
}
5478
$event->setValue($existingValue[0]['configvalue']);
5579
return;
5680
}

lib/Listener/DeclarativeSettings/SetValueListener.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@
1414
use OCA\AppAPI\Service\UI\SettingsService;
1515
use OCP\EventDispatcher\Event;
1616
use OCP\EventDispatcher\IEventListener;
17+
use OCP\Security\ICrypto;
1718
use OCP\Settings\DeclarativeSettingsTypes;
1819
use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
20+
use Psr\Log\LoggerInterface;
1921

2022
/**
2123
* @template-implements IEventListener<Event>
2224
*/
2325
class SetValueListener implements IEventListener {
2426
public function __construct(
25-
private readonly SettingsService $service,
27+
private readonly SettingsService $service,
2628
private readonly ExAppPreferenceService $preferenceService,
27-
private readonly ExAppConfigService $configService,
29+
private readonly ExAppConfigService $configService,
30+
private readonly ICrypto $crypto,
31+
private readonly LoggerInterface $logger,
2832
) {
2933
}
3034

@@ -38,11 +42,25 @@ public function handle(Event $event): void {
3842
return;
3943
}
4044
$formSchema = $settingsForm->getScheme();
45+
$field = $settingsForm->getSchemaField($event->getFieldId());
46+
$isSensitive = isset($field['sensitive']) && $field['sensitive'] === true;
47+
$value = $event->getValue();
48+
if ($isSensitive) {
49+
try {
50+
$value = $this->crypto->encrypt($value);
51+
} catch (\Exception $e) {
52+
$this->logger->warning(
53+
sprintf('Failed to encrypt sensitive value for app %s, field %s', $event->getApp(), $event->getFieldId()),
54+
['exception' => $e, 'app' => $event->getApp()]
55+
);
56+
return;
57+
}
58+
}
4159
if ($formSchema['section_type'] === DeclarativeSettingsTypes::SECTION_TYPE_ADMIN) {
42-
$this->configService->setAppConfigValue($event->getApp(), $event->getFieldId(), $event->getValue());
60+
$this->configService->setAppConfigValue($event->getApp(), $event->getFieldId(), $value);
4361
} else {
4462
$this->preferenceService->setUserConfigValue(
45-
$event->getUser()->getUID(), $event->getApp(), $event->getFieldId(), $event->getValue()
63+
$event->getUser()->getUID(), $event->getApp(), $event->getFieldId(), $value
4664
);
4765
}
4866
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\AppAPI\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\IDBConnection;
16+
use OCP\Migration\IOutput;
17+
use OCP\Migration\SimpleMigrationStep;
18+
use OCP\Security\ICrypto;
19+
20+
class Version032002Date20250527174907 extends SimpleMigrationStep {
21+
22+
public function __construct(
23+
private IDBConnection $connection,
24+
private ICrypto $crypto,
25+
) {
26+
}
27+
28+
/**
29+
* @param IOutput $output
30+
* @param Closure(): ISchemaWrapper $schemaClosure
31+
* @param array $options
32+
* @return null|ISchemaWrapper
33+
*/
34+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
35+
/** @var ISchemaWrapper $schema */
36+
$schema = $schemaClosure();
37+
38+
if ($schema->hasTable('preferences_ex')) {
39+
$table = $schema->getTable('preferences_ex');
40+
41+
if (!$table->hasColumn('sensitive')) {
42+
$table->addColumn('sensitive', Types::SMALLINT, [
43+
'notnull' => true,
44+
'default' => 0,
45+
]);
46+
}
47+
}
48+
49+
return $schema;
50+
}
51+
52+
/**
53+
* @param IOutput $output
54+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
55+
* @param array $options
56+
*
57+
* @return null|ISchemaWrapper
58+
*/
59+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
60+
// encrypt appconfig_ex values that have sensitive flag set
61+
62+
$qbSelect = $this->connection->getQueryBuilder();
63+
$qbSelect->select(['id', 'configvalue'])
64+
->from('appconfig_ex')
65+
->where($qbSelect->expr()->eq('sensitive', $qbSelect->createNamedParameter(1, Types::SMALLINT)));
66+
$req = $qbSelect->executeQuery();
67+
68+
while ($row = $req->fetch()) {
69+
$configValue = $row['configvalue'];
70+
if (!empty($configValue)) {
71+
try {
72+
$encryptedValue = $this->crypto->encrypt($configValue);
73+
$qbUpdate = $this->connection->getQueryBuilder();
74+
$qbUpdate->update('appconfig_ex')
75+
->set('configvalue', $qbUpdate->createNamedParameter($encryptedValue))
76+
->where(
77+
$qbUpdate->expr()->eq('id', $qbUpdate->createNamedParameter($row['id'], Types::INTEGER))
78+
);
79+
$qbUpdate->executeStatement();
80+
} catch (\Exception $e) {
81+
$output->warning(sprintf('Failed to encrypt sensitive value for app config id %s: %s', $row['id'], $e->getMessage()));
82+
}
83+
}
84+
}
85+
86+
$req->closeCursor();
87+
return null;
88+
}
89+
}

lib/Service/ExAppConfigService.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCP\AppFramework\Db\DoesNotExistException;
1616
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
1717
use OCP\DB\Exception;
18+
use OCP\Security\ICrypto;
1819
use Psr\Log\LoggerInterface;
1920

2021
/**
@@ -25,15 +26,25 @@ class ExAppConfigService {
2526
public function __construct(
2627
private ExAppConfigMapper $mapper,
2728
private LoggerInterface $logger,
29+
private ICrypto $crypto,
2830
) {
2931
}
3032

3133
public function getAppConfigValues(string $appId, array $configKeys): ?array {
3234
try {
3335
return array_map(function (ExAppConfig $exAppConfig) {
36+
$value = $exAppConfig->getConfigvalue() ?? '';
37+
if ($value !== '' && $exAppConfig->getSensitive()) {
38+
try {
39+
$value = $this->crypto->decrypt($value);
40+
} catch (\Exception $e) {
41+
$this->logger->warning(sprintf('Failed to decrypt sensitive value for app %s, config key %s', $exAppConfig->getAppid(), $exAppConfig->getConfigkey()), ['exception' => $e]);
42+
$value = '';
43+
}
44+
}
3445
return [
3546
'configkey' => $exAppConfig->getConfigkey(),
36-
'configvalue' => $exAppConfig->getConfigvalue() ?? '',
47+
'configvalue' => $value,
3748
];
3849
}, $this->mapper->findByAppConfigKeys($appId, $configKeys));
3950
} catch (Exception) {
@@ -43,20 +54,30 @@ public function getAppConfigValues(string $appId, array $configKeys): ?array {
4354

4455
public function setAppConfigValue(string $appId, string $configKey, mixed $configValue, ?int $sensitive = null): ?ExAppConfig {
4556
$appConfigEx = $this->getAppConfig($appId, $configKey);
57+
if ($configValue !== '' && $sensitive) {
58+
try {
59+
$encryptedValue = $this->crypto->encrypt($configValue);
60+
} catch (\Exception $e) {
61+
$this->logger->error(sprintf('Failed to encrypt sensitive value for app %s, config key %s. Error: %s', $appId, $configKey, $e->getMessage()), ['exception' => $e]);
62+
return null;
63+
}
64+
} else {
65+
$encryptedValue = '';
66+
}
4667
if ($appConfigEx === null) {
4768
try {
4869
$appConfigEx = $this->mapper->insert(new ExAppConfig([
4970
'appid' => $appId,
5071
'configkey' => $configKey,
51-
'configvalue' => $configValue ?? '',
72+
'configvalue' => $sensitive ? $encryptedValue : $configValue ?? '',
5273
'sensitive' => $sensitive ?? 0,
5374
]));
5475
} catch (Exception $e) {
5576
$this->logger->error(sprintf('Failed to insert appconfig_ex value. Error: %s', $e->getMessage()), ['exception' => $e]);
5677
return null;
5778
}
5879
} else {
59-
$appConfigEx->setConfigvalue($configValue);
80+
$appConfigEx->setConfigvalue($sensitive ? $encryptedValue : $configValue);
6081
if ($sensitive !== null) {
6182
$appConfigEx->setSensitive($sensitive);
6283
}
@@ -65,6 +86,10 @@ public function setAppConfigValue(string $appId, string $configKey, mixed $confi
6586
return null;
6687
}
6788
}
89+
if ($sensitive) {
90+
// setting original unencrypted value for API
91+
$appConfigEx->setConfigvalue($configValue);
92+
}
6893
return $appConfigEx;
6994
}
7095

0 commit comments

Comments
 (0)