MigrateKeyStorage.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Core\Command\Encryption;
  8. use OC\Encryption\Keys\Storage;
  9. use OC\Encryption\Util;
  10. use OC\Files\View;
  11. use OCP\IConfig;
  12. use OCP\IUserManager;
  13. use OCP\Security\ICrypto;
  14. use Symfony\Component\Console\Command\Command;
  15. use Symfony\Component\Console\Helper\ProgressBar;
  16. use Symfony\Component\Console\Input\InputInterface;
  17. use Symfony\Component\Console\Output\OutputInterface;
  18. class MigrateKeyStorage extends Command {
  19. public function __construct(
  20. protected View $rootView,
  21. protected IUserManager $userManager,
  22. protected IConfig $config,
  23. protected Util $util,
  24. private ICrypto $crypto,
  25. ) {
  26. parent::__construct();
  27. }
  28. protected function configure(): void {
  29. parent::configure();
  30. $this
  31. ->setName('encryption:migrate-key-storage-format')
  32. ->setDescription('Migrate the format of the keystorage to a newer format');
  33. }
  34. protected function execute(InputInterface $input, OutputInterface $output): int {
  35. $root = $this->util->getKeyStorageRoot();
  36. $output->writeln('Updating key storage format');
  37. $this->updateKeys($root, $output);
  38. $output->writeln('Key storage format successfully updated');
  39. return 0;
  40. }
  41. /**
  42. * Move keys to new key storage root
  43. *
  44. * @throws \Exception
  45. */
  46. protected function updateKeys(string $root, OutputInterface $output): bool {
  47. $output->writeln('Start to update the keys:');
  48. $this->updateSystemKeys($root, $output);
  49. $this->updateUsersKeys($root, $output);
  50. $this->config->deleteSystemValue('encryption.key_storage_migrated');
  51. return true;
  52. }
  53. /**
  54. * Move system key folder
  55. */
  56. protected function updateSystemKeys(string $root, OutputInterface $output): void {
  57. if (!$this->rootView->is_dir($root . '/files_encryption')) {
  58. return;
  59. }
  60. $this->traverseKeys($root . '/files_encryption', null, $output);
  61. }
  62. private function traverseKeys(string $folder, ?string $uid, OutputInterface $output): void {
  63. $listing = $this->rootView->getDirectoryContent($folder);
  64. foreach ($listing as $node) {
  65. if ($node['mimetype'] === 'httpd/unix-directory') {
  66. continue;
  67. }
  68. if ($node['name'] === 'fileKey' ||
  69. str_ends_with($node['name'], '.privateKey') ||
  70. str_ends_with($node['name'], '.publicKey') ||
  71. str_ends_with($node['name'], '.shareKey')) {
  72. $path = $folder . '/' . $node['name'];
  73. $content = $this->rootView->file_get_contents($path);
  74. if ($content === false) {
  75. $output->writeln("<error>Failed to open path $path</error>");
  76. continue;
  77. }
  78. try {
  79. $this->crypto->decrypt($content);
  80. continue;
  81. } catch (\Exception $e) {
  82. // Ignore we now update the data.
  83. }
  84. $data = [
  85. 'key' => base64_encode($content),
  86. 'uid' => $uid,
  87. ];
  88. $enc = $this->crypto->encrypt(json_encode($data));
  89. $this->rootView->file_put_contents($path, $enc);
  90. }
  91. }
  92. }
  93. private function traverseFileKeys(string $folder, OutputInterface $output): void {
  94. $listing = $this->rootView->getDirectoryContent($folder);
  95. foreach ($listing as $node) {
  96. if ($node['mimetype'] === 'httpd/unix-directory') {
  97. $this->traverseFileKeys($folder . '/' . $node['name'], $output);
  98. } else {
  99. $endsWith = function (string $haystack, string $needle): bool {
  100. $length = strlen($needle);
  101. if ($length === 0) {
  102. return true;
  103. }
  104. return (substr($haystack, -$length) === $needle);
  105. };
  106. if ($node['name'] === 'fileKey' ||
  107. $endsWith($node['name'], '.privateKey') ||
  108. $endsWith($node['name'], '.publicKey') ||
  109. $endsWith($node['name'], '.shareKey')) {
  110. $path = $folder . '/' . $node['name'];
  111. $content = $this->rootView->file_get_contents($path);
  112. if ($content === false) {
  113. $output->writeln("<error>Failed to open path $path</error>");
  114. continue;
  115. }
  116. try {
  117. $this->crypto->decrypt($content);
  118. continue;
  119. } catch (\Exception $e) {
  120. // Ignore we now update the data.
  121. }
  122. $data = [
  123. 'key' => base64_encode($content)
  124. ];
  125. $enc = $this->crypto->encrypt(json_encode($data));
  126. $this->rootView->file_put_contents($path, $enc);
  127. }
  128. }
  129. }
  130. }
  131. /**
  132. * setup file system for the given user
  133. */
  134. protected function setupUserFS(string $uid): void {
  135. \OC_Util::tearDownFS();
  136. \OC_Util::setupFS($uid);
  137. }
  138. /**
  139. * iterate over each user and move the keys to the new storage
  140. */
  141. protected function updateUsersKeys(string $root, OutputInterface $output): void {
  142. $progress = new ProgressBar($output);
  143. $progress->start();
  144. foreach ($this->userManager->getBackends() as $backend) {
  145. $limit = 500;
  146. $offset = 0;
  147. do {
  148. $users = $backend->getUsers('', $limit, $offset);
  149. foreach ($users as $user) {
  150. $progress->advance();
  151. $this->setupUserFS($user);
  152. $this->updateUserKeys($root, $user, $output);
  153. }
  154. $offset += $limit;
  155. } while (count($users) >= $limit);
  156. }
  157. $progress->finish();
  158. }
  159. /**
  160. * move user encryption folder to new root folder
  161. *
  162. * @throws \Exception
  163. */
  164. protected function updateUserKeys(string $root, string $user, OutputInterface $output): void {
  165. if ($this->userManager->userExists($user)) {
  166. $source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE';
  167. if ($this->rootView->is_dir($source)) {
  168. $this->traverseKeys($source, $user, $output);
  169. }
  170. $source = $root . '/' . $user . '/files_encryption/keys';
  171. if ($this->rootView->is_dir($source)) {
  172. $this->traverseFileKeys($source, $output);
  173. }
  174. }
  175. }
  176. }