ChangeKeyStorageRoot.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Core\Command\Encryption;
  8. use OC\Encryption\Keys\Storage;
  9. use OC\Encryption\Util;
  10. use OC\Files\Filesystem;
  11. use OC\Files\View;
  12. use OCP\IConfig;
  13. use OCP\IUserManager;
  14. use Symfony\Component\Console\Command\Command;
  15. use Symfony\Component\Console\Helper\ProgressBar;
  16. use Symfony\Component\Console\Helper\QuestionHelper;
  17. use Symfony\Component\Console\Input\InputArgument;
  18. use Symfony\Component\Console\Input\InputInterface;
  19. use Symfony\Component\Console\Output\OutputInterface;
  20. use Symfony\Component\Console\Question\ConfirmationQuestion;
  21. class ChangeKeyStorageRoot extends Command {
  22. public function __construct(
  23. protected View $rootView,
  24. protected IUserManager $userManager,
  25. protected IConfig $config,
  26. protected Util $util,
  27. protected QuestionHelper $questionHelper,
  28. ) {
  29. parent::__construct();
  30. }
  31. protected function configure() {
  32. parent::configure();
  33. $this
  34. ->setName('encryption:change-key-storage-root')
  35. ->setDescription('Change key storage root')
  36. ->addArgument(
  37. 'newRoot',
  38. InputArgument::OPTIONAL,
  39. 'new root of the key storage relative to the data folder'
  40. );
  41. }
  42. protected function execute(InputInterface $input, OutputInterface $output): int {
  43. $oldRoot = $this->util->getKeyStorageRoot();
  44. $newRoot = $input->getArgument('newRoot');
  45. if ($newRoot === null) {
  46. $question = new ConfirmationQuestion('No storage root given, do you want to reset the key storage root to the default location? (y/n) ', false);
  47. if (!$this->questionHelper->ask($input, $output, $question)) {
  48. return 1;
  49. }
  50. $newRoot = '';
  51. }
  52. $oldRootDescription = $oldRoot !== '' ? $oldRoot : 'default storage location';
  53. $newRootDescription = $newRoot !== '' ? $newRoot : 'default storage location';
  54. $output->writeln("Change key storage root from <info>$oldRootDescription</info> to <info>$newRootDescription</info>");
  55. $success = $this->moveAllKeys($oldRoot, $newRoot, $output);
  56. if ($success) {
  57. $this->util->setKeyStorageRoot($newRoot);
  58. $output->writeln('');
  59. $output->writeln("Key storage root successfully changed to <info>$newRootDescription</info>");
  60. return 0;
  61. }
  62. return 1;
  63. }
  64. /**
  65. * move keys to new key storage root
  66. *
  67. * @param string $oldRoot
  68. * @param string $newRoot
  69. * @param OutputInterface $output
  70. * @return bool
  71. * @throws \Exception
  72. */
  73. protected function moveAllKeys($oldRoot, $newRoot, OutputInterface $output) {
  74. $output->writeln('Start to move keys:');
  75. if ($this->rootView->is_dir($oldRoot) === false) {
  76. $output->writeln('No old keys found: Nothing needs to be moved');
  77. return false;
  78. }
  79. $this->prepareNewRoot($newRoot);
  80. $this->moveSystemKeys($oldRoot, $newRoot);
  81. $this->moveUserKeys($oldRoot, $newRoot, $output);
  82. return true;
  83. }
  84. /**
  85. * prepare new key storage
  86. *
  87. * @param string $newRoot
  88. * @throws \Exception
  89. */
  90. protected function prepareNewRoot($newRoot) {
  91. if ($this->rootView->is_dir($newRoot) === false) {
  92. throw new \Exception("New root folder doesn't exist. Please create the folder or check the permissions and try again.");
  93. }
  94. $result = $this->rootView->file_put_contents(
  95. $newRoot . '/' . Storage::KEY_STORAGE_MARKER,
  96. 'Nextcloud will detect this folder as key storage root only if this file exists'
  97. );
  98. if (!$result) {
  99. throw new \Exception("Can't access the new root folder. Please check the permissions and make sure that the folder is in your data folder");
  100. }
  101. }
  102. /**
  103. * move system key folder
  104. *
  105. * @param string $oldRoot
  106. * @param string $newRoot
  107. */
  108. protected function moveSystemKeys($oldRoot, $newRoot) {
  109. if (
  110. $this->rootView->is_dir($oldRoot . '/files_encryption') &&
  111. $this->targetExists($newRoot . '/files_encryption') === false
  112. ) {
  113. $this->rootView->rename($oldRoot . '/files_encryption', $newRoot . '/files_encryption');
  114. }
  115. }
  116. /**
  117. * setup file system for the given user
  118. *
  119. * @param string $uid
  120. */
  121. protected function setupUserFS($uid) {
  122. \OC_Util::tearDownFS();
  123. \OC_Util::setupFS($uid);
  124. }
  125. /**
  126. * iterate over each user and move the keys to the new storage
  127. *
  128. * @param string $oldRoot
  129. * @param string $newRoot
  130. * @param OutputInterface $output
  131. */
  132. protected function moveUserKeys($oldRoot, $newRoot, OutputInterface $output) {
  133. $progress = new ProgressBar($output);
  134. $progress->start();
  135. foreach ($this->userManager->getBackends() as $backend) {
  136. $limit = 500;
  137. $offset = 0;
  138. do {
  139. $users = $backend->getUsers('', $limit, $offset);
  140. foreach ($users as $user) {
  141. $progress->advance();
  142. $this->setupUserFS($user);
  143. $this->moveUserEncryptionFolder($user, $oldRoot, $newRoot);
  144. }
  145. $offset += $limit;
  146. } while (count($users) >= $limit);
  147. }
  148. $progress->finish();
  149. }
  150. /**
  151. * move user encryption folder to new root folder
  152. *
  153. * @param string $user
  154. * @param string $oldRoot
  155. * @param string $newRoot
  156. * @throws \Exception
  157. */
  158. protected function moveUserEncryptionFolder($user, $oldRoot, $newRoot) {
  159. if ($this->userManager->userExists($user)) {
  160. $source = $oldRoot . '/' . $user . '/files_encryption';
  161. $target = $newRoot . '/' . $user . '/files_encryption';
  162. if (
  163. $this->rootView->is_dir($source) &&
  164. $this->targetExists($target) === false
  165. ) {
  166. $this->prepareParentFolder($newRoot . '/' . $user);
  167. $this->rootView->rename($source, $target);
  168. }
  169. }
  170. }
  171. /**
  172. * Make preparations to filesystem for saving a key file
  173. *
  174. * @param string $path relative to data/
  175. */
  176. protected function prepareParentFolder($path) {
  177. $path = Filesystem::normalizePath($path);
  178. // If the file resides within a subdirectory, create it
  179. if ($this->rootView->file_exists($path) === false) {
  180. $sub_dirs = explode('/', ltrim($path, '/'));
  181. $dir = '';
  182. foreach ($sub_dirs as $sub_dir) {
  183. $dir .= '/' . $sub_dir;
  184. if ($this->rootView->file_exists($dir) === false) {
  185. $this->rootView->mkdir($dir);
  186. }
  187. }
  188. }
  189. }
  190. /**
  191. * check if target already exists
  192. *
  193. * @param $path
  194. * @return bool
  195. * @throws \Exception
  196. */
  197. protected function targetExists($path) {
  198. if ($this->rootView->file_exists($path)) {
  199. throw new \Exception("new folder '$path' already exists");
  200. }
  201. return false;
  202. }
  203. }