diff options
author | Roeland Jago Douma <roeland@famdouma.nl> | 2020-06-22 16:35:52 +0300 |
---|---|---|
committer | Roeland Jago Douma <roeland@famdouma.nl> | 2020-08-20 16:42:43 +0300 |
commit | 5340ab3a75d58651e3cc65688d94444b38570cfc (patch) | |
tree | 426f4cd4a944319ed1254a463adfbb86dba6f950 | |
parent | 886466d5109de6ed399e2da3dcf87eea66d531ce (diff) |
New SSE key format
* Encrypt the keys with the instance secret
* Store them as json (so we can add other things if needed)
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
-rw-r--r-- | core/Command/Encryption/MigrateKeyStorage.php | 261 | ||||
-rw-r--r-- | core/register_command.php | 8 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 2 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 2 | ||||
-rw-r--r-- | lib/private/Encryption/Keys/Storage.php | 146 | ||||
-rw-r--r-- | lib/private/Repair.php | 2 | ||||
-rw-r--r-- | lib/private/Repair/NC20/EncryptionMigration.php | 62 | ||||
-rw-r--r-- | lib/private/Server.php | 2 | ||||
-rw-r--r-- | tests/lib/Encryption/Keys/StorageTest.php | 127 | ||||
-rw-r--r-- | version.php | 2 |
10 files changed, 584 insertions, 30 deletions
diff --git a/core/Command/Encryption/MigrateKeyStorage.php b/core/Command/Encryption/MigrateKeyStorage.php new file mode 100644 index 00000000000..099eba1e822 --- /dev/null +++ b/core/Command/Encryption/MigrateKeyStorage.php @@ -0,0 +1,261 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Keys\Storage; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\IConfig; +use OCP\IUserManager; +use OCP\Security\ICrypto; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrateKeyStorage extends Command { + + /** @var View */ + protected $rootView; + + /** @var IUserManager */ + protected $userManager; + + /** @var IConfig */ + protected $config; + + /** @var Util */ + protected $util; + + /** @var QuestionHelper */ + protected $questionHelper; + /** + * @var ICrypto + */ + private $crypto; + + public function __construct(View $view, IUserManager $userManager, IConfig $config, Util $util, ICrypto $crypto) { + parent::__construct(); + $this->rootView = $view; + $this->userManager = $userManager; + $this->config = $config; + $this->util = $util; + $this->crypto = $crypto; + } + + protected function configure() { + parent::configure(); + $this + ->setName('encryption:migrate-key-storage-format') + ->setDescription('Migrate the format of the keystorage to a newer format'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $root = $this->util->getKeyStorageRoot(); + + $output->writeln("Updating key storage format"); + $this->updateKeys($root, $output); + $output->writeln("Key storage format successfully updated"); + + return 0; + } + + /** + * move keys to new key storage root + * + * @param string $root + * @param OutputInterface $output + * @return bool + * @throws \Exception + */ + protected function updateKeys(string $root, OutputInterface $output) { + $output->writeln("Start to update the keys:"); + + $this->updateSystemKeys($root); + $this->updateUsersKeys($root, $output); + $this->config->deleteSystemValue('encryption.key_storage_migrated'); + return true; + } + + /** + * move system key folder + * + * @param string $root + */ + protected function updateSystemKeys($root) { + if (!$this->rootView->is_dir($root . '/files_encryption')) { + return; + } + + $this->traverseKeys($root . '/files_encryption', null); + } + + private function traverseKeys(string $folder, ?string $uid) { + $listing = $this->rootView->getDirectoryContent($folder); + + foreach ($listing as $node) { + if ($node['mimetype'] === 'httpd/unix-directory') { + //ignore + } else { + $endsWith = function ($haystack, $needle) { + $length = strlen($needle); + if ($length === 0) { + return true; + } + + return (substr($haystack, -$length) === $needle); + }; + + if ($node['name'] === 'fileKey' || + $endsWith($node['name'], '.privateKey') || + $endsWith($node['name'], '.publicKey') || + $endsWith($node['name'], '.shareKey')) { + $path = $folder . '/' . $node['name']; + + $content = $this->rootView->file_get_contents($path); + + try { + $this->crypto->decrypt($content); + continue; + } catch (\Exception $e) { + // Ignore we now update the data. + } + + $data = [ + 'key' => base64_encode($content), + 'uid' => $uid, + ]; + + $enc = $this->crypto->encrypt(json_encode($data)); + $this->rootView->file_put_contents($path, $enc); + } + } + } + } + + private function traverseFileKeys(string $folder) { + $listing = $this->rootView->getDirectoryContent($folder); + + foreach ($listing as $node) { + if ($node['mimetype'] === 'httpd/unix-directory') { + $this->traverseFileKeys($folder . '/' . $node['name']); + } else { + $endsWith = function ($haystack, $needle) { + $length = strlen($needle); + if ($length === 0) { + return true; + } + + return (substr($haystack, -$length) === $needle); + }; + + if ($node['name'] === 'fileKey' || + $endsWith($node['name'], '.privateKey') || + $endsWith($node['name'], '.publicKey') || + $endsWith($node['name'], '.shareKey')) { + $path = $folder . '/' . $node['name']; + + $content = $this->rootView->file_get_contents($path); + + try { + $this->crypto->decrypt($content); + continue; + } catch (\Exception $e) { + // Ignore we now update the data. + } + + $data = [ + 'key' => base64_encode($content) + ]; + + $enc = $this->crypto->encrypt(json_encode($data)); + $this->rootView->file_put_contents($path, $enc); + } + } + } + } + + + /** + * setup file system for the given user + * + * @param string $uid + */ + protected function setupUserFS($uid) { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($uid); + } + + + /** + * iterate over each user and move the keys to the new storage + * + * @param string $root + * @param OutputInterface $output + */ + protected function updateUsersKeys(string $root, OutputInterface $output) { + $progress = new ProgressBar($output); + $progress->start(); + + foreach ($this->userManager->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $progress->advance(); + $this->setupUserFS($user); + $this->updateUserKeys($root, $user); + } + $offset += $limit; + } while (count($users) >= $limit); + } + $progress->finish(); + } + + /** + * move user encryption folder to new root folder + * + * @param string $root + * @param string $user + * @throws \Exception + */ + protected function updateUserKeys(string $root, string $user) { + if ($this->userManager->userExists($user)) { + $source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE'; + if ($this->rootView->is_dir($source)) { + $this->traverseKeys($source, $user); + } + + $source = $root . '/' . $user . '/files_encryption/keys'; + if ($this->rootView->is_dir($source)) { + $this->traverseFileKeys($source); + } + } + } +} diff --git a/core/register_command.php b/core/register_command.php index c2a3f76b6c3..e05cff3e37f 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -139,6 +139,14 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { ) ); $application->add(new OC\Core\Command\Encryption\ShowKeyStorageRoot($util)); + $application->add(new OC\Core\Command\Encryption\MigrateKeyStorage( + $view, + \OC::$server->getUserManager(), + \OC::$server->getConfig(), + $util, + \OC::$server->getCrypto() + ) + ); $application->add(new OC\Core\Command\Maintenance\DataFingerprint(\OC::$server->getConfig(), new \OC\AppFramework\Utility\TimeFactory())); $application->add(new OC\Core\Command\Maintenance\Mimetype\UpdateDB(\OC::$server->getMimeTypeDetector(), \OC::$server->getMimeTypeLoader())); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d3fb9181c3b..c0e35fa4604 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -808,6 +808,7 @@ return array( 'OC\\Core\\Command\\Encryption\\Enable' => $baseDir . '/core/Command/Encryption/Enable.php', 'OC\\Core\\Command\\Encryption\\EncryptAll' => $baseDir . '/core/Command/Encryption/EncryptAll.php', 'OC\\Core\\Command\\Encryption\\ListModules' => $baseDir . '/core/Command/Encryption/ListModules.php', + 'OC\\Core\\Command\\Encryption\\MigrateKeyStorage' => $baseDir . '/core/Command/Encryption/MigrateKeyStorage.php', 'OC\\Core\\Command\\Encryption\\SetDefaultModule' => $baseDir . '/core/Command/Encryption/SetDefaultModule.php', 'OC\\Core\\Command\\Encryption\\ShowKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ShowKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\Status' => $baseDir . '/core/Command/Encryption/Status.php', @@ -1248,6 +1249,7 @@ return array( 'OC\\Repair\\NC16\\ClearCollectionsAccessCache' => $baseDir . '/lib/private/Repair/NC16/ClearCollectionsAccessCache.php', 'OC\\Repair\\NC18\\ResetGeneratedAvatarFlag' => $baseDir . '/lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php', 'OC\\Repair\\NC20\\EncryptionLegacyCipher' => $baseDir . '/lib/private/Repair/NC20/EncryptionLegacyCipher.php', + 'OC\\Repair\\NC20\\EncryptionMigration' => $baseDir . '/lib/private/Repair/NC20/EncryptionMigration.php', 'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\DropAccountTermsTable' => $baseDir . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php', 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => $baseDir . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index e0ac8a31147..a1786bc1de0 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -837,6 +837,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\Encryption\\Enable' => __DIR__ . '/../../..' . '/core/Command/Encryption/Enable.php', 'OC\\Core\\Command\\Encryption\\EncryptAll' => __DIR__ . '/../../..' . '/core/Command/Encryption/EncryptAll.php', 'OC\\Core\\Command\\Encryption\\ListModules' => __DIR__ . '/../../..' . '/core/Command/Encryption/ListModules.php', + 'OC\\Core\\Command\\Encryption\\MigrateKeyStorage' => __DIR__ . '/../../..' . '/core/Command/Encryption/MigrateKeyStorage.php', 'OC\\Core\\Command\\Encryption\\SetDefaultModule' => __DIR__ . '/../../..' . '/core/Command/Encryption/SetDefaultModule.php', 'OC\\Core\\Command\\Encryption\\ShowKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ShowKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\Status' => __DIR__ . '/../../..' . '/core/Command/Encryption/Status.php', @@ -1277,6 +1278,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Repair\\NC16\\ClearCollectionsAccessCache' => __DIR__ . '/../../..' . '/lib/private/Repair/NC16/ClearCollectionsAccessCache.php', 'OC\\Repair\\NC18\\ResetGeneratedAvatarFlag' => __DIR__ . '/../../..' . '/lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php', 'OC\\Repair\\NC20\\EncryptionLegacyCipher' => __DIR__ . '/../../..' . '/lib/private/Repair/NC20/EncryptionLegacyCipher.php', + 'OC\\Repair\\NC20\\EncryptionMigration' => __DIR__ . '/../../..' . '/lib/private/Repair/NC20/EncryptionMigration.php', 'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\DropAccountTermsTable' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php', 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php', diff --git a/lib/private/Encryption/Keys/Storage.php b/lib/private/Encryption/Keys/Storage.php index cee32691261..43a291b886c 100644 --- a/lib/private/Encryption/Keys/Storage.php +++ b/lib/private/Encryption/Keys/Storage.php @@ -31,8 +31,11 @@ namespace OC\Encryption\Keys; use OC\Encryption\Util; use OC\Files\Filesystem; use OC\Files\View; +use OC\ServerNotAvailableException; use OC\User\NoUserException; use OCP\Encryption\Keys\IStorage; +use OCP\IConfig; +use OCP\Security\ICrypto; class Storage implements IStorage { @@ -62,11 +65,17 @@ class Storage implements IStorage { /** @var array */ private $keyCache = []; + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + /** * @param View $view * @param Util $util */ - public function __construct(View $view, Util $util) { + public function __construct(View $view, Util $util, ICrypto $crypto, IConfig $config) { $this->view = $view; $this->util = $util; @@ -74,6 +83,8 @@ class Storage implements IStorage { $this->keys_base_dir = $this->encryption_base_dir .'/keys'; $this->backup_base_dir = $this->encryption_base_dir .'/backup'; $this->root_dir = $this->util->getKeyStorageRoot(); + $this->crypto = $crypto; + $this->config = $config; } /** @@ -81,7 +92,7 @@ class Storage implements IStorage { */ public function getUserKey($uid, $keyId, $encryptionModuleId) { $path = $this->constructUserKeyPath($encryptionModuleId, $keyId, $uid); - return $this->getKey($path); + return base64_decode($this->getKeyWithUid($path, $uid)); } /** @@ -90,17 +101,17 @@ class Storage implements IStorage { public function getFileKey($path, $keyId, $encryptionModuleId) { $realFile = $this->util->stripPartialFileExtension($path); $keyDir = $this->getFileKeyDir($encryptionModuleId, $realFile); - $key = $this->getKey($keyDir . $keyId); + $key = $this->getKey($keyDir . $keyId)['key']; if ($key === '' && $realFile !== $path) { // Check if the part file has keys and use them, if no normal keys // exist. This is required to fix copyBetweenStorage() when we // rename a .part file over storage borders. $keyDir = $this->getFileKeyDir($encryptionModuleId, $path); - $key = $this->getKey($keyDir . $keyId); + $key = $this->getKey($keyDir . $keyId)['key']; } - return $key; + return base64_decode($key); } /** @@ -108,7 +119,7 @@ class Storage implements IStorage { */ public function getSystemUserKey($keyId, $encryptionModuleId) { $path = $this->constructUserKeyPath($encryptionModuleId, $keyId, null); - return $this->getKey($path); + return base64_decode($this->getKeyWithUid($path, null)); } /** @@ -116,7 +127,10 @@ class Storage implements IStorage { */ public function setUserKey($uid, $keyId, $key, $encryptionModuleId) { $path = $this->constructUserKeyPath($encryptionModuleId, $keyId, $uid); - return $this->setKey($path, $key); + return $this->setKey($path, [ + 'key' => base64_encode($key), + 'uid' => $uid, + ]); } /** @@ -124,7 +138,9 @@ class Storage implements IStorage { */ public function setFileKey($path, $keyId, $key, $encryptionModuleId) { $keyDir = $this->getFileKeyDir($encryptionModuleId, $path); - return $this->setKey($keyDir . $keyId, $key); + return $this->setKey($keyDir . $keyId, [ + 'key' => base64_encode($key), + ]); } /** @@ -132,7 +148,10 @@ class Storage implements IStorage { */ public function setSystemUserKey($keyId, $key, $encryptionModuleId) { $path = $this->constructUserKeyPath($encryptionModuleId, $keyId, null); - return $this->setKey($path, $key); + return $this->setKey($path, [ + 'key' => base64_encode($key), + 'uid' => null, + ]); } /** @@ -200,19 +219,106 @@ class Storage implements IStorage { } /** + * @param string $path + * @param string|null $uid + * @return string + * @throws ServerNotAvailableException + * + * Small helper function to fetch the key and verify the value for user and system keys + */ + private function getKeyWithUid(string $path, ?string $uid): string { + $data = $this->getKey($path); + + if (!isset($data['key'])) { + throw new ServerNotAvailableException('Key is invalid'); + } + + if ($data['key'] === '') { + return ''; + } + + if (!array_key_exists('uid', $data) || $data['uid'] !== $uid) { + // If the migration is done we error out + $versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0'); + if (version_compare($versionFromBeforeUpdate, '20.0.0.1', '<=')) { + return $data['key']; + } + + if ($this->config->getSystemValueBool('encryption.key_storage_migrated', true)) { + throw new ServerNotAvailableException('Key has been modified'); + } else { + //Otherwise we migrate + $data['uid'] = $uid; + $this->setKey($path, $data); + } + } + + return $data['key']; + } + + /** * read key from hard disk * * @param string $path to key - * @return string + * @return array containing key as base64encoded key, and possible the uid */ - private function getKey($path) { - $key = ''; + private function getKey($path): array { + $key = [ + 'key' => '', + ]; if ($this->view->file_exists($path)) { if (isset($this->keyCache[$path])) { $key = $this->keyCache[$path]; } else { - $key = $this->view->file_get_contents($path); + $data = $this->view->file_get_contents($path); + + // Version <20.0.0.1 doesn't have this + $versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0'); + if (version_compare($versionFromBeforeUpdate, '20.0.0.1', '<=')) { + $key = [ + 'key' => base64_encode($data), + ]; + } else { + if ($this->config->getSystemValueBool('encryption.key_storage_migrated', true)) { + try { + $clearData = $this->crypto->decrypt($data); + } catch (\Exception $e) { + throw new ServerNotAvailableException('Could not decrypt key', 0, $e); + } + + $dataArray = json_decode($clearData, true); + if ($dataArray === null) { + throw new ServerNotAvailableException('Invalid encryption key'); + } + + $key = $dataArray; + } else { + /* + * Even if not all keys are migrated we should still try to decrypt it (in case some have moved). + * However it is only a failure now if it is an array and decryption fails + */ + $fallback = false; + try { + $clearData = $this->crypto->decrypt($data); + } catch (\Exception $e) { + $fallback = true; + } + + if (!$fallback) { + $dataArray = json_decode($clearData, true); + if ($dataArray === null) { + throw new ServerNotAvailableException('Invalid encryption key'); + } + $key = $dataArray; + } else { + $key = [ + 'key' => base64_encode($data), + ]; + } + } + } + $this->keyCache[$path] = $key; } } @@ -225,13 +331,23 @@ class Storage implements IStorage { * * * @param string $path path to key directory - * @param string $key key + * @param array $key key * @return bool */ private function setKey($path, $key) { $this->keySetPreparation(dirname($path)); - $result = $this->view->file_put_contents($path, $key); + $versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0'); + if (version_compare($versionFromBeforeUpdate, '20.0.0.1', '<=')) { + // Only store old format if this happens during the migration. + // TODO: Remove for 21 + $data = base64_decode($key['key']); + } else { + // Wrap the data + $data = $this->crypto->encrypt(json_encode($key)); + } + + $result = $this->view->file_put_contents($path, $data); if (is_int($result) && $result > 0) { $this->keyCache[$path] = $key; diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 6151812d316..4ad21e74238 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -49,6 +49,7 @@ use OC\Repair\NC16\CleanupCardDAVPhotoCache; use OC\Repair\NC16\ClearCollectionsAccessCache; use OC\Repair\NC18\ResetGeneratedAvatarFlag; use OC\Repair\NC20\EncryptionLegacyCipher; +use OC\Repair\NC20\EncryptionMigration; use OC\Repair\OldGroupMembershipShares; use OC\Repair\Owncloud\DropAccountTermsTable; use OC\Repair\Owncloud\SaveAccountsTableData; @@ -158,6 +159,7 @@ class Repair implements IOutput { new ClearCollectionsAccessCache(\OC::$server->getConfig(), \OC::$server->query(IManager::class)), \OC::$server->query(ResetGeneratedAvatarFlag::class), \OC::$server->query(EncryptionLegacyCipher::class), + \OC::$server->query(EncryptionMigration::class), ]; } diff --git a/lib/private/Repair/NC20/EncryptionMigration.php b/lib/private/Repair/NC20/EncryptionMigration.php new file mode 100644 index 00000000000..6d5c2dc0c58 --- /dev/null +++ b/lib/private/Repair/NC20/EncryptionMigration.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Repair\NC20; + +use OCP\Encryption\IManager; +use OCP\IConfig; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class EncryptionMigration implements IRepairStep { + + /** @var IConfig */ + private $config; + /** @var IManager */ + private $manager; + + public function __construct(IConfig $config, + IManager $manager) { + $this->config = $config; + $this->manager = $manager; + } + + public function getName(): string { + return 'Check encryption key format'; + } + + private function shouldRun(): bool { + $versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0'); + return version_compare($versionFromBeforeUpdate, '20.0.0.1', '<='); + } + + public function run(IOutput $output): void { + if ($this->manager->isEnabled()) { + if ($this->config->getSystemValue('encryption.key_storage_migrated', '') === '') { + $this->config->setSystemValue('encryption.key_storage_migrated', false); + } + } + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index a934628a047..9403aa9212b 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -348,7 +348,7 @@ class Server extends ServerContainer implements IServerContainer { $c->getConfig() ); - return new Encryption\Keys\Storage($view, $util); + return new Encryption\Keys\Storage($view, $util, $c->getCrypto(), $c->getConfig()); }); /** @deprecated 20.0.0 */ $this->registerDeprecatedAlias('TagMapper', TagMapper::class); diff --git a/tests/lib/Encryption/Keys/StorageTest.php b/tests/lib/Encryption/Keys/StorageTest.php index bd9bbdecdf1..fdfa08d87c4 100644 --- a/tests/lib/Encryption/Keys/StorageTest.php +++ b/tests/lib/Encryption/Keys/StorageTest.php @@ -26,6 +26,8 @@ namespace Test\Encryption\Keys; use OC\Encryption\Keys\Storage; use OC\Files\View; use OCP\IConfig; +use OCP\Security\ICrypto; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class StorageTest extends TestCase { @@ -42,6 +44,9 @@ class StorageTest extends TestCase { /** @var \PHPUnit\Framework\MockObject\MockObject */ protected $config; + /** @var MockObject|ICrypto */ + protected $crypto; + protected function setUp(): void { parent::setUp(); @@ -53,14 +58,53 @@ class StorageTest extends TestCase { ->disableOriginalConstructor() ->getMock(); + $this->crypto = $this->createMock(ICrypto::class); + $this->crypto->method('encrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + $this->crypto->method('decrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + $this->config = $this->getMockBuilder(IConfig::class) ->disableOriginalConstructor() ->getMock(); - $this->storage = new Storage($this->view, $this->util); + $this->storage = new Storage($this->view, $this->util, $this->crypto, $this->config); } public function testSetFileKey() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(['user1', '/files/foo.txt']); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + + $data = json_encode(['key' => base64_encode('key')]); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo($data)) + ->willReturn(strlen($data)); + + $this->assertTrue( + $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') + ); + } + + public function testSetFileOld() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.0'); $this->util->expects($this->any()) ->method('getUidAndFilename') ->willReturn(['user1', '/files/foo.txt']); @@ -70,6 +114,8 @@ class StorageTest extends TestCase { $this->util->expects($this->any()) ->method('isSystemWideMountPoint') ->willReturn(false); + $this->crypto->expects($this->never()) + ->method('encrypt'); $this->view->expects($this->once()) ->method('file_put_contents') ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), @@ -98,6 +144,9 @@ class StorageTest extends TestCase { * @param string $expectedKeyContent */ public function testGetFileKey($path, $strippedPartialName, $originalKeyExists, $expectedKeyContent) { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); $this->util->expects($this->any()) ->method('getUidAndFilename') ->willReturnMap([ @@ -118,6 +167,11 @@ class StorageTest extends TestCase { ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) ->willReturn($originalKeyExists); + $this->crypto->method('decrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + if (!$originalKeyExists) { $this->view->expects($this->at(1)) ->method('file_exists') @@ -127,12 +181,12 @@ class StorageTest extends TestCase { $this->view->expects($this->once()) ->method('file_get_contents') ->with($this->equalTo('/user1/files_encryption/keys' . $path . '/encModule/fileKey')) - ->willReturn('key2'); + ->willReturn(json_encode(['key' => base64_encode('key2')])); } else { $this->view->expects($this->once()) ->method('file_get_contents') ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) - ->willReturn('key'); + ->willReturn(json_encode(['key' => base64_encode('key')])); } $this->assertSame($expectedKeyContent, @@ -141,6 +195,10 @@ class StorageTest extends TestCase { } public function testSetFileKeySystemWide() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + $this->util->expects($this->any()) ->method('getUidAndFilename') ->willReturn(['user1', '/files/foo.txt']); @@ -150,11 +208,18 @@ class StorageTest extends TestCase { $this->util->expects($this->any()) ->method('stripPartialFileExtension') ->willReturnArgument(0); + + $this->crypto->method('encrypt') + ->willReturnCallback(function ($data, $pass) { + return $data; + }); + + $data = json_encode(['key' => base64_encode('key')]); $this->view->expects($this->once()) ->method('file_put_contents') ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey'), - $this->equalTo('key')) - ->willReturn(strlen('key')); + $this->equalTo($data)) + ->willReturn(strlen($data)); $this->assertTrue( $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') @@ -162,6 +227,10 @@ class StorageTest extends TestCase { } public function testGetFileKeySystemWide() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + $this->util->expects($this->any()) ->method('getUidAndFilename') ->willReturn(['user1', '/files/foo.txt']); @@ -174,7 +243,7 @@ class StorageTest extends TestCase { $this->view->expects($this->once()) ->method('file_get_contents') ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn('key'); + ->willReturn(json_encode(['key' => base64_encode('key')])); $this->view->expects($this->once()) ->method('file_exists') ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) @@ -186,11 +255,19 @@ class StorageTest extends TestCase { } public function testSetSystemUserKey() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => null] + ); $this->view->expects($this->once()) ->method('file_put_contents') ->with($this->equalTo('/files_encryption/encModule/shareKey_56884'), - $this->equalTo('key')) - ->willReturn(strlen('key')); + $this->equalTo($data)) + ->willReturn(strlen($data)); $this->assertTrue( $this->storage->setSystemUserKey('shareKey_56884', 'key', 'encModule') @@ -198,11 +275,19 @@ class StorageTest extends TestCase { } public function testSetUserKey() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => 'user1'] + ); $this->view->expects($this->once()) ->method('file_put_contents') ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey'), - $this->equalTo('key')) - ->willReturn(strlen('key')); + $this->equalTo($data)) + ->willReturn(strlen($data)); $this->assertTrue( $this->storage->setUserKey('user1', 'publicKey', 'key', 'encModule') @@ -210,10 +295,18 @@ class StorageTest extends TestCase { } public function testGetSystemUserKey() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => null] + ); $this->view->expects($this->once()) ->method('file_get_contents') ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) - ->willReturn('key'); + ->willReturn($data); $this->view->expects($this->once()) ->method('file_exists') ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) @@ -225,10 +318,18 @@ class StorageTest extends TestCase { } public function testGetUserKey() { + $this->config->method('getSystemValue') + ->with('version') + ->willReturn('20.0.0.2'); + + $data = json_encode([ + 'key' => base64_encode('key'), + 'uid' => 'user1'] + ); $this->view->expects($this->once()) ->method('file_get_contents') ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) - ->willReturn('key'); + ->willReturn($data); $this->view->expects($this->once()) ->method('file_exists') ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) @@ -516,7 +617,7 @@ class StorageTest extends TestCase { */ public function testBackupUserKeys($createBackupDir) { $storage = $this->getMockBuilder('OC\Encryption\Keys\Storage') - ->setConstructorArgs([$this->view, $this->util]) + ->setConstructorArgs([$this->view, $this->util, $this->crypto, $this->config]) ->setMethods(['getTimestamp']) ->getMock(); diff --git a/version.php b/version.php index 9df6d8fbecc..7f4d76bea22 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = [20, 0, 0, 1]; +$OC_Version = [20, 0, 0, 2]; // The human readable string $OC_VersionString = '20.0.0 alpha'; |