MigrateKeyStorage.php 5.2 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() {
  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. * @param string $root
  45. * @param OutputInterface $output
  46. * @return bool
  47. * @throws \Exception
  48. */
  49. protected function updateKeys(string $root, OutputInterface $output): bool {
  50. $output->writeln('Start to update the keys:');
  51. $this->updateSystemKeys($root);
  52. $this->updateUsersKeys($root, $output);
  53. $this->config->deleteSystemValue('encryption.key_storage_migrated');
  54. return true;
  55. }
  56. /**
  57. * Move system key folder
  58. */
  59. protected function updateSystemKeys(string $root): void {
  60. if (!$this->rootView->is_dir($root . '/files_encryption')) {
  61. return;
  62. }
  63. $this->traverseKeys($root . '/files_encryption', null);
  64. }
  65. private function traverseKeys(string $folder, ?string $uid) {
  66. $listing = $this->rootView->getDirectoryContent($folder);
  67. foreach ($listing as $node) {
  68. if ($node['mimetype'] === 'httpd/unix-directory') {
  69. continue;
  70. }
  71. if ($node['name'] === 'fileKey' ||
  72. str_ends_with($node['name'], '.privateKey') ||
  73. str_ends_with($node['name'], '.publicKey') ||
  74. str_ends_with($node['name'], '.shareKey')) {
  75. $path = $folder . '/' . $node['name'];
  76. $content = $this->rootView->file_get_contents($path);
  77. try {
  78. $this->crypto->decrypt($content);
  79. continue;
  80. } catch (\Exception $e) {
  81. // Ignore we now update the data.
  82. }
  83. $data = [
  84. 'key' => base64_encode($content),
  85. 'uid' => $uid,
  86. ];
  87. $enc = $this->crypto->encrypt(json_encode($data));
  88. $this->rootView->file_put_contents($path, $enc);
  89. }
  90. }
  91. }
  92. private function traverseFileKeys(string $folder) {
  93. $listing = $this->rootView->getDirectoryContent($folder);
  94. foreach ($listing as $node) {
  95. if ($node['mimetype'] === 'httpd/unix-directory') {
  96. $this->traverseFileKeys($folder . '/' . $node['name']);
  97. } else {
  98. $endsWith = function ($haystack, $needle) {
  99. $length = strlen($needle);
  100. if ($length === 0) {
  101. return true;
  102. }
  103. return (substr($haystack, -$length) === $needle);
  104. };
  105. if ($node['name'] === 'fileKey' ||
  106. $endsWith($node['name'], '.privateKey') ||
  107. $endsWith($node['name'], '.publicKey') ||
  108. $endsWith($node['name'], '.shareKey')) {
  109. $path = $folder . '/' . $node['name'];
  110. $content = $this->rootView->file_get_contents($path);
  111. try {
  112. $this->crypto->decrypt($content);
  113. continue;
  114. } catch (\Exception $e) {
  115. // Ignore we now update the data.
  116. }
  117. $data = [
  118. 'key' => base64_encode($content)
  119. ];
  120. $enc = $this->crypto->encrypt(json_encode($data));
  121. $this->rootView->file_put_contents($path, $enc);
  122. }
  123. }
  124. }
  125. }
  126. /**
  127. * setup file system for the given user
  128. *
  129. * @param string $uid
  130. */
  131. protected function setupUserFS($uid) {
  132. \OC_Util::tearDownFS();
  133. \OC_Util::setupFS($uid);
  134. }
  135. /**
  136. * iterate over each user and move the keys to the new storage
  137. *
  138. * @param string $root
  139. * @param OutputInterface $output
  140. */
  141. protected function updateUsersKeys(string $root, OutputInterface $output) {
  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);
  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. * @param string $root
  163. * @param string $user
  164. * @throws \Exception
  165. */
  166. protected function updateUserKeys(string $root, string $user) {
  167. if ($this->userManager->userExists($user)) {
  168. $source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE';
  169. if ($this->rootView->is_dir($source)) {
  170. $this->traverseKeys($source, $user);
  171. }
  172. $source = $root . '/' . $user . '/files_encryption/keys';
  173. if ($this->rootView->is_dir($source)) {
  174. $this->traverseFileKeys($source);
  175. }
  176. }
  177. }
  178. }