diff --git a/apps/encryption/appinfo/info.xml b/apps/encryption/appinfo/info.xml index e77261c471207..de7ba7f078331 100644 --- a/apps/encryption/appinfo/info.xml +++ b/apps/encryption/appinfo/info.xml @@ -42,6 +42,7 @@ Please read the documentation to know all implications before you decide to enab OCA\Encryption\Command\ScanLegacyFormat OCA\Encryption\Command\FixEncryptedVersion OCA\Encryption\Command\FixKeyLocation + OCA\Encryption\Command\DropLegacyFileKey diff --git a/apps/encryption/composer/composer/autoload_classmap.php b/apps/encryption/composer/composer/autoload_classmap.php index 9f9ab4e406f3d..059296338b49a 100644 --- a/apps/encryption/composer/composer/autoload_classmap.php +++ b/apps/encryption/composer/composer/autoload_classmap.php @@ -9,6 +9,7 @@ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php', + 'OCA\\Encryption\\Command\\DropLegacyFileKey' => $baseDir . '/../lib/Command/DropLegacyFileKey.php', 'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.php', 'OCA\\Encryption\\Command\\FixEncryptedVersion' => $baseDir . '/../lib/Command/FixEncryptedVersion.php', 'OCA\\Encryption\\Command\\FixKeyLocation' => $baseDir . '/../lib/Command/FixKeyLocation.php', diff --git a/apps/encryption/composer/composer/autoload_static.php b/apps/encryption/composer/composer/autoload_static.php index 8f50f0649970f..6c458eabddd1a 100644 --- a/apps/encryption/composer/composer/autoload_static.php +++ b/apps/encryption/composer/composer/autoload_static.php @@ -24,6 +24,7 @@ class ComposerStaticInitEncryption 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php', + 'OCA\\Encryption\\Command\\DropLegacyFileKey' => __DIR__ . '/..' . '/../lib/Command/DropLegacyFileKey.php', 'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.php', 'OCA\\Encryption\\Command\\FixEncryptedVersion' => __DIR__ . '/..' . '/../lib/Command/FixEncryptedVersion.php', 'OCA\\Encryption\\Command\\FixKeyLocation' => __DIR__ . '/..' . '/../lib/Command/FixKeyLocation.php', diff --git a/apps/encryption/composer/composer/installed.php b/apps/encryption/composer/composer/installed.php index 1426826287dc4..46be34510db8b 100644 --- a/apps/encryption/composer/composer/installed.php +++ b/apps/encryption/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'dd3d689e04a5e1d558da937ca72980e0e2c7c404', + 'reference' => 'ffa8d21f37c8ccf968974b6aeb828e3e84287b94', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'dd3d689e04a5e1d558da937ca72980e0e2c7c404', + 'reference' => 'ffa8d21f37c8ccf968974b6aeb828e3e84287b94', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/encryption/lib/Command/DropLegacyFileKey.php b/apps/encryption/lib/Command/DropLegacyFileKey.php new file mode 100644 index 0000000000000..f0a5f36f30fee --- /dev/null +++ b/apps/encryption/lib/Command/DropLegacyFileKey.php @@ -0,0 +1,167 @@ + + * + * @author Côme Chilliet + * + * @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 . + * + */ + +namespace OCA\Encryption\Command; + +use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Files\FileInfo; +use OC\Files\View; +use OCA\Encryption\KeyManager; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DropLegacyFileKey extends Command { + private View $rootView; + + public function __construct( + private IUserManager $userManager, + private KeyManager $keyManager, + ) { + parent::__construct(); + + $this->rootView = new View(); + } + + protected function configure(): void { + $this + ->setName('encryption:drop-legacy-filekey') + ->setDescription('Scan the files for the legacy filekey format using RC4 and get rid of it (if master key is enabled)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $result = true; + + $output->writeln('Scanning all files for legacy filekey'); + + foreach ($this->userManager->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $output->writeln('Scanning all files for ' . $user); + $this->setupUserFS($user); + $result = $result && $this->scanFolder($output, '/' . $user); + } + $offset += $limit; + } while (count($users) >= $limit); + } + + if ($result) { + $output->writeln('All scanned files are properly encrypted.'); + return 0; + } + + return 1; + } + + private function scanFolder(OutputInterface $output, string $folder): bool { + $clean = true; + + foreach ($this->rootView->getDirectoryContent($folder) as $item) { + $path = $folder . '/' . $item['name']; + if ($this->rootView->is_dir($path)) { + if ($this->scanFolder($output, $path) === false) { + $clean = false; + } + } else { + if (!$item->isEncrypted()) { + // ignore + continue; + } + + $stats = $this->rootView->stat($path); + if (!isset($stats['hasHeader']) || $stats['hasHeader'] === false) { + $clean = false; + $output->writeln('' . $path . ' does not have a proper header'); + } else { + try { + $legacyFileKey = $this->keyManager->getFileKey($path, null, true); + if ($legacyFileKey === '') { + $output->writeln('Got an empty legacy filekey for ' . $path . ', continuing', OutputInterface::VERBOSITY_VERBOSE); + continue; + } + } catch (GenericEncryptionException $e) { + $output->writeln('Got a decryption error for legacy filekey for ' . $path . ', continuing', OutputInterface::VERBOSITY_VERBOSE); + continue; + } + /* If that did not throw and filekey is not empty, a legacy filekey is used */ + $clean = false; + $output->writeln($path . ' is using a legacy filekey, migrating'); + $this->migrateSinglefile($path, $item, $output); + } + } + } + + return $clean; + } + + private function migrateSinglefile(string $path, FileInfo $fileInfo, OutputInterface $output): void { + $source = $path; + $target = $path . '.reencrypted.' . time(); + + try { + $this->rootView->copy($source, $target); + $copyResource = $this->rootView->fopen($target, 'r'); + $sourceResource = $this->rootView->fopen($source, 'w'); + if ($copyResource === false || $sourceResource === false) { + throw new DecryptionFailedException('Failed to open '.$source.' or '.$target); + } + if (stream_copy_to_stream($copyResource, $sourceResource) === false) { + $output->writeln('Failed to copy '.$target.' data into '.$source.''); + $output->writeln('Leaving both files in there to avoid data loss'); + return; + } + $this->rootView->touch($source, $fileInfo->getMTime()); + $this->rootView->unlink($target); + $output->writeln('Migrated ' . $source . '', OutputInterface::VERBOSITY_VERBOSE); + } catch (DecryptionFailedException $e) { + if ($this->rootView->file_exists($target)) { + $this->rootView->unlink($target); + } + $output->writeln('Failed to migrate ' . $path . ''); + $output->writeln('' . $e . '', OutputInterface::VERBOSITY_VERBOSE); + } finally { + if (is_resource($copyResource)) { + fclose($copyResource); + } + if (is_resource($sourceResource)) { + fclose($sourceResource); + } + } + } + + /** + * setup user file system + */ + protected function setupUserFS(string $uid): void { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($uid); + } +} diff --git a/apps/encryption/lib/Command/ScanLegacyFormat.php b/apps/encryption/lib/Command/ScanLegacyFormat.php index dc6d43ee5b870..85a99a178453c 100644 --- a/apps/encryption/lib/Command/ScanLegacyFormat.php +++ b/apps/encryption/lib/Command/ScanLegacyFormat.php @@ -36,7 +36,6 @@ use Symfony\Component\Console\Output\OutputInterface; class ScanLegacyFormat extends Command { - /** @var Util */ protected $util; @@ -89,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($users as $user) { $output->writeln('Scanning all files for ' . $user); $this->setupUserFS($user); - $result &= $this->scanFolder($output, '/' . $user); + $result = $result && $this->scanFolder($output, '/' . $user); } $offset += $limit; } while (count($users) >= $limit); diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index 465856fd28d33..0bcaa167907f1 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -309,7 +309,12 @@ public function end($path, $position = '0') { $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path)); $shareKeys = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); - $this->keyManager->deleteLegacyFileKey($this->path); + if (!$this->keyManager->deleteLegacyFileKey($this->path)) { + $this->logger->warning( + 'Failed to delete legacy filekey for {path}', + ['app' => 'encryption', 'path' => $path] + ); + } foreach ($shareKeys as $uid => $keyFile) { $this->keyManager->setShareKey($this->path, $uid, $keyFile); }