diff --git a/lib/Migration/Version032002Date20250527174907.php b/lib/Migration/Version032002Date20250527174907.php index 51c88bda9..9e0c61e9a 100644 --- a/lib/Migration/Version032002Date20250527174907.php +++ b/lib/Migration/Version032002Date20250527174907.php @@ -18,6 +18,7 @@ use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; use OCP\Security\ICrypto; +use Throwable; #[AddColumn('preferences_ex', 'sensitive', ColumnType::SMALLINT, 'support sensitive setting')] class Version032002Date20250527174907 extends SimpleMigrationStep { @@ -71,6 +72,11 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array while ($row = $req->fetch()) { $configValue = $row['configvalue']; if (!empty($configValue)) { + // Readers decrypt exactly once, so re-encrypting an already-encrypted value would + // nest a second envelope and make the stored secret unrecoverable on the next read. + if ($this->isEncrypted((string)$configValue)) { + continue; + } try { $encryptedValue = $this->crypto->encrypt($configValue); $qbUpdate = $this->connection->getQueryBuilder(); @@ -89,4 +95,17 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $req->closeCursor(); return null; } + + /** + * ICrypto::decrypt() HMAC-verifies the envelope, so a value that decrypts cleanly is already + * encrypted and must not be encrypted again. + */ + private function isEncrypted(string $value): bool { + try { + $this->crypto->decrypt($value); + return true; + } catch (Throwable) { + return false; + } + } } diff --git a/lib/Migration/Version2000Date20240120094952.php b/lib/Migration/Version2000Date20240120094952.php index bcc7badec..33269e80d 100644 --- a/lib/Migration/Version2000Date20240120094952.php +++ b/lib/Migration/Version2000Date20240120094952.php @@ -26,20 +26,30 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_apps'); - if ($table->hasColumn('protocol')) { - $table->dropColumn('protocol'); + if ($schema->hasTable('ex_apps')) { + $table = $schema->getTable('ex_apps'); + if ($table->hasColumn('protocol')) { + $table->dropColumn('protocol'); + } + if ($table->hasColumn('host')) { + $table->dropColumn('host'); + } + // would result in re-creation of the same index if this migration is re-run + // but should be safe to run. + if ($table->hasIndex('ex_apps_c_port__idx')) { + $table->dropIndex('ex_apps_c_port__idx'); + } + $table->addUniqueIndex(['daemon_config_name', 'port'], 'ex_apps_c_port__idx'); } - if ($table->hasColumn('host')) { - $table->dropColumn('host'); - } - $table->dropIndex('ex_apps_c_port__idx'); - $table->addUniqueIndex(['daemon_config_name', 'port'], 'ex_apps_c_port__idx'); - $table = $schema->getTable('ex_apps_daemons'); - $table->changeColumn('deploy_config', [ - 'notnull' => true, - ]); + if ($schema->hasTable('ex_apps_daemons')) { + $table = $schema->getTable('ex_apps_daemons'); + if ($table->hasColumn('deploy_config')) { + $table->changeColumn('deploy_config', [ + 'notnull' => true, + ]); + } + } return $schema; } diff --git a/lib/Migration/Version2201Date20240221124152.php b/lib/Migration/Version2201Date20240221124152.php index f65f4d885..211003e54 100644 --- a/lib/Migration/Version2201Date20240221124152.php +++ b/lib/Migration/Version2201Date20240221124152.php @@ -27,14 +27,18 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_apps'); - - $table->addColumn('is_system', Types::SMALLINT, [ - 'notnull' => true, - 'default' => 0, - 'length' => 1, - 'unsigned' => true, - ]); + if ($schema->hasTable('ex_apps')) { + $table = $schema->getTable('ex_apps'); + + if (!$table->hasColumn('is_system')) { + $table->addColumn('is_system', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 0, + 'length' => 1, + 'unsigned' => true, + ]); + } + } return $schema; } diff --git a/lib/Migration/Version2203Date20240325124149.php b/lib/Migration/Version2203Date20240325124149.php index 5efde824e..b000465d4 100644 --- a/lib/Migration/Version2203Date20240325124149.php +++ b/lib/Migration/Version2203Date20240325124149.php @@ -27,11 +27,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_apps'); - - $table->addColumn('api_scopes', Types::JSON, [ - 'notnull' => false, - ]); + if ($schema->hasTable('ex_apps')) { + $table = $schema->getTable('ex_apps'); + + if (!$table->hasColumn('api_scopes')) { + $table->addColumn('api_scopes', Types::JSON, [ + 'notnull' => false, + ]); + } + } return $schema; } diff --git a/lib/Migration/Version2206Date20240502145029.php b/lib/Migration/Version2206Date20240502145029.php index e4ff2cb73..7be6c1ec2 100644 --- a/lib/Migration/Version2206Date20240502145029.php +++ b/lib/Migration/Version2206Date20240502145029.php @@ -30,11 +30,13 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if ($schema->hasTable('ex_ui_files_actions')) { $table = $schema->getTable('ex_ui_files_actions'); - $table->addColumn('version', Types::STRING, [ - 'notnull' => true, - 'length' => 64, - 'default' => '1.0', - ]); + if (!$table->hasColumn('version')) { + $table->addColumn('version', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + 'default' => '1.0', + ]); + } } return $schema; diff --git a/lib/Migration/Version2800Date20240711080316.php b/lib/Migration/Version2800Date20240711080316.php index 4cfdd7055..e0746e10b 100644 --- a/lib/Migration/Version2800Date20240711080316.php +++ b/lib/Migration/Version2800Date20240711080316.php @@ -26,11 +26,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_task_processing'); - if (!$table->hasColumn('custom_task_type')) { - $table->addColumn('custom_task_type', Types::TEXT, [ - 'notnull' => false, - ]); + if ($schema->hasTable('ex_task_processing')) { + $table = $schema->getTable('ex_task_processing'); + + if (!$table->hasColumn('custom_task_type')) { + $table->addColumn('custom_task_type', Types::TEXT, [ + 'notnull' => false, + ]); + } } return $schema; diff --git a/lib/Migration/Version3000Date20240807085759.php b/lib/Migration/Version3000Date20240807085759.php index 609e9d73e..b6f60c39f 100644 --- a/lib/Migration/Version3000Date20240807085759.php +++ b/lib/Migration/Version3000Date20240807085759.php @@ -26,11 +26,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_task_processing'); - if (!$table->hasColumn('provider')) { - $table->addColumn('provider', Types::TEXT, [ - 'notnull' => true, - ]); + if ($schema->hasTable('ex_task_processing')) { + $table = $schema->getTable('ex_task_processing'); + + if (!$table->hasColumn('provider')) { + $table->addColumn('provider', Types::TEXT, [ + 'notnull' => true, + ]); + } } return $schema; diff --git a/lib/Migration/Version3100Date20240822080316.php b/lib/Migration/Version3100Date20240822080316.php index 68252cb37..f11303b99 100644 --- a/lib/Migration/Version3100Date20240822080316.php +++ b/lib/Migration/Version3100Date20240822080316.php @@ -26,12 +26,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_apps_routes'); - if (!$table->hasColumn('bruteforce_protection')) { - $table->addColumn('bruteforce_protection', Types::STRING, [ - 'notnull' => false, - 'length' => 512, - ]); + if ($schema->hasTable('ex_apps_routes')) { + $table = $schema->getTable('ex_apps_routes'); + + if (!$table->hasColumn('bruteforce_protection')) { + $table->addColumn('bruteforce_protection', Types::STRING, [ + 'notnull' => false, + 'length' => 512, + ]); + } } return $schema; diff --git a/lib/Migration/Version3200Date20240905080316.php b/lib/Migration/Version3200Date20240905080316.php index 41b5b8716..680bbef5a 100644 --- a/lib/Migration/Version3200Date20240905080316.php +++ b/lib/Migration/Version3200Date20240905080316.php @@ -25,12 +25,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - $table = $schema->getTable('ex_apps'); - if ($table->hasColumn('last_check_time')) { - $table->dropColumn('last_check_time'); - } - if ($table->hasColumn('api_scopes')) { - $table->dropColumn('api_scopes'); + if ($schema->hasTable('ex_apps')) { + $table = $schema->getTable('ex_apps'); + + if ($table->hasColumn('last_check_time')) { + $table->dropColumn('last_check_time'); + } + if ($table->hasColumn('api_scopes')) { + $table->dropColumn('api_scopes'); + } } return $schema; diff --git a/lib/Migration/Version5000Date20241120135411.php b/lib/Migration/Version5000Date20241120135411.php index c32d819cc..e995b7846 100644 --- a/lib/Migration/Version5000Date20241120135411.php +++ b/lib/Migration/Version5000Date20241120135411.php @@ -15,6 +15,7 @@ use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; use OCP\Security\ICrypto; +use Throwable; class Version5000Date20241120135411 extends SimpleMigrationStep { @@ -42,6 +43,12 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $deployConfig = $row['deploy_config']; $deployConfig = json_decode($deployConfig, true); if (!empty($deployConfig['haproxy_password'])) { + // Every consumer decrypts exactly once, so a value that is already ciphertext must be + // left alone: wrapping a second envelope around it silently turns the daemon key into + // garbage. This step is replayed whenever the migration was applied but not recorded. + if ($this->isEncrypted((string)$deployConfig['haproxy_password'])) { + continue; + } $deployConfig['haproxy_password'] = $this->crypto->encrypt($deployConfig['haproxy_password']); $encodedDeployConfig = json_encode($deployConfig); $qbUpdate = $this->connection->getQueryBuilder(); @@ -56,4 +63,17 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $req->closeCursor(); return null; } + + /** + * ICrypto::decrypt() HMAC-verifies the envelope, so a value that decrypts cleanly is already + * encrypted and must not be encrypted again. + */ + private function isEncrypted(string $value): bool { + try { + $this->crypto->decrypt($value); + return true; + } catch (Throwable) { + return false; + } + } }