Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 129 additions & 73 deletions apps/encryption/lib/Command/FixKeyLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
use OCP\Files\IRootFolder;
use OCP\Files\ISetupManager;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -39,6 +41,7 @@ public function __construct(
private readonly Util $encryptionUtil,
private readonly IRootFolder $rootFolder,
private readonly ISetupManager $setupManager,
private readonly LoggerInterface $logger,
IManager $encryptionManager,
) {
$this->keyRootDirectory = rtrim($this->encryptionUtil->getKeyStorageRoot(), '/');
Expand Down Expand Up @@ -75,8 +78,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->setupManager->setupForUser($user);

$mounts = $this->getSystemMountsForUser($user);
$failedPaths = [];
foreach ($mounts as $mount) {
$mountRootFolder = $this->rootFolder->get($mount->getMountPoint());
try {
$mountRootFolder = $this->rootFolder->get($mount->getMountPoint());
} catch (NotFoundException $e) {
$this->logger->warning('Mount point of user ' . $user->getUID() . ' not found: ' . $mount->getMountPoint(), [
'app' => 'encryption',
'exception' => $e,
]);
$output->writeln('<error>System wide mount point not found, skipping: ' . $mount->getMountPoint() . '</error>');
continue;
}
if (!$mountRootFolder instanceof Folder) {
$output->writeln('<error>System wide mount point is not a directory, skipping: ' . $mount->getMountPoint() . '</error>');
continue;
Expand All @@ -85,75 +98,97 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$files = $this->getAllEncryptedFiles($mountRootFolder);
foreach ($files as $file) {
/** @var File $file */
$hasSystemKey = $this->hasSystemKey($file);
$hasUserKey = $this->hasUserKey($user, $file);
if (!$hasSystemKey) {
if ($hasUserKey) {
// key was stored incorrectly as user key, migrate

if ($dryRun) {
$output->writeln('<info>' . $file->getPath() . '</info> needs migration');
try {
$this->fixKeysForFile($user, $file, $dryRun, $output);
} catch (\Throwable $e) {
$failedPaths[] = $file->getPath();
$this->logger->error('Failed to fix the key location of ' . $file->getPath(), [
'app' => 'encryption',
'exception' => $e,
]);
$output->writeln('<error>Failed to process ' . $file->getPath() . ': ' . $e->getMessage() . '</error>');
}
}
}

if ($failedPaths !== []) {
$output->writeln('');
$output->writeln('<error>' . count($failedPaths) . ' file(s) could not be processed, see the log for details:</error>');
foreach ($failedPaths as $failedPath) {
$output->writeln('<error> - ' . $failedPath . '</error>');
}
return self::FAILURE;
}

return self::SUCCESS;
}

private function fixKeysForFile(IUser $user, File $file, bool $dryRun, OutputInterface $output): void {
$hasSystemKey = $this->hasSystemKey($file);
$hasUserKey = $this->hasUserKey($user, $file);
if (!$hasSystemKey) {
if ($hasUserKey) {
// key was stored incorrectly as user key, migrate

if ($dryRun) {
$output->writeln('<info>' . $file->getPath() . '</info> needs migration');
} else {
$output->write('Migrating key for <info>' . $file->getPath() . '</info> ');
if ($this->copyUserKeyToSystemAndValidate($user, $file)) {
$output->writeln('<info>✓</info>');
} else {
$output->writeln('<fg=red>❌</>');
$output->writeln(' Failed to validate key for <error>' . $file->getPath() . '</error>, key will not be migrated');
}
}
} else {
// no matching key, probably from a broken cross-storage move

$shouldBeEncrypted = $file->getStorage()->instanceOfStorage(Encryption::class);
$isActuallyEncrypted = $this->isDataEncrypted($file);
if ($isActuallyEncrypted) {
if ($dryRun) {
if ($shouldBeEncrypted) {
$output->write('<info>' . $file->getPath() . '</info> needs migration');
} else {
$output->write('Migrating key for <info>' . $file->getPath() . '</info> ');
if ($this->copyUserKeyToSystemAndValidate($user, $file)) {
$output->writeln('<info>✓</info>');
} else {
$output->writeln('<fg=red>❌</>');
$output->writeln(' Failed to validate key for <error>' . $file->getPath() . '</error>, key will not be migrated');
}
$output->write('<info>' . $file->getPath() . '</info> needs decryption');
}
$foundKey = $this->findUserKeyForSystemFile($user, $file);
if ($foundKey) {
$output->writeln(', valid key found at <info>' . $foundKey . '</info>');
} else {
$output->writeln(' <error>❌ No key found</error>');
}
} else {
// no matching key, probably from a broken cross-storage move

$shouldBeEncrypted = $file->getStorage()->instanceOfStorage(Encryption::class);
$isActuallyEncrypted = $this->isDataEncrypted($file);
if ($isActuallyEncrypted) {
if ($dryRun) {
if ($shouldBeEncrypted) {
$output->write('<info>' . $file->getPath() . '</info> needs migration');
} else {
$output->write('<info>' . $file->getPath() . '</info> needs decryption');
}
$foundKey = $this->findUserKeyForSystemFile($user, $file);
if ($foundKey) {
$output->writeln(', valid key found at <info>' . $foundKey . '</info>');
} else {
$output->writeln(' <error>❌ No key found</error>');
}
} else {
if ($shouldBeEncrypted) {
$output->write('<info>Migrating key for ' . $file->getPath() . '</info>');
} else {
$output->write('<info>Decrypting ' . $file->getPath() . '</info>');
}
$foundKey = $this->findUserKeyForSystemFile($user, $file);
if ($foundKey) {
if ($shouldBeEncrypted) {
$systemKeyPath = $this->getSystemKeyPath($file);
$this->rootView->copy($foundKey, $systemKeyPath);
$output->writeln(' Migrated key from <info>' . $foundKey . '</info>');
} else {
$this->decryptWithSystemKey($file, $foundKey);
$output->writeln(' Decrypted with key from <info>' . $foundKey . '</info>');
}
} else {
$output->writeln(' <error>❌ No key found</error>');
}
}
if ($shouldBeEncrypted) {
$output->write('<info>Migrating key for ' . $file->getPath() . '</info>');
} else {
if ($dryRun) {
$output->writeln('<info>' . $file->getPath() . ' needs to be marked as not encrypted</info>');
$output->write('<info>Decrypting ' . $file->getPath() . '</info>');
}
$foundKey = $this->findUserKeyForSystemFile($user, $file);
if ($foundKey) {
if ($shouldBeEncrypted) {
$systemKeyPath = $this->getSystemKeyPath($file);
$this->rootView->copy($foundKey, $systemKeyPath);
$output->writeln(' Migrated key from <info>' . $foundKey . '</info>');
} else {
$this->markAsUnEncrypted($file);
$output->writeln('<info>' . $file->getPath() . ' marked as not encrypted</info>');
$this->decryptWithSystemKey($file, $foundKey);
$output->writeln(' Decrypted with key from <info>' . $foundKey . '</info>');
}
} else {
$output->writeln(' <error>❌ No key found</error>');
}
}
} else {
if ($dryRun) {
$output->writeln('<info>' . $file->getPath() . ' needs to be marked as not encrypted</info>');
} else {
$this->markAsUnEncrypted($file);
$output->writeln('<info>' . $file->getPath() . ' marked as not encrypted</info>');
}
}
}
}

return self::SUCCESS;
}

private function getUserRelativePath(string $path): string {
Expand All @@ -168,7 +203,7 @@ private function getUserRelativePath(string $path): string {
/**
* @return ICachedMountInfo[]
*/
private function getSystemMountsForUser(IUser $user): array {
protected function getSystemMountsForUser(IUser $user): array {
return array_filter($this->userMountCache->getMountsForUser($user), function (ICachedMountInfo $mount) use (
$user
) {
Expand Down Expand Up @@ -241,15 +276,19 @@ private function copyUserKeyToSystemAndValidate(IUser $user, File $node): bool {

private function tryReadFile(File $node): bool {
try {
$fh = $node->fopen('r');
// read a single chunk
$data = fread($fh, 8192);
if ($data === false) {
// a raw read on an unwrapped mount would succeed with any key
$storage = $node->getStorage();
if (!$storage->instanceOfStorage(Encryption::class)) {
$storage = $this->encryptionManager->forceWrapStorage($node->getMountPoint(), $storage);
}
$fh = $storage->fopen($node->getInternalPath(), 'r');
if ($fh === false) {
return false;
} else {
return true;
}
} catch (\Exception $e) {
$data = fread($fh, 8192);
fclose($fh);
return $data !== false;
} catch (\Exception) {
return false;
}
}
Expand Down Expand Up @@ -314,6 +353,10 @@ private function findUserKeyForSystemFile(IUser $user, File $node): ?string {
* @return \Generator<string>
*/
private function findKeysByFileName(string $basePath, string $name) {
if (!$this->rootView->is_dir($basePath)) {
// no keys stored for the user at all
return;
}
if ($this->rootView->is_dir($basePath . '/' . $name . '/OC_DEFAULT_MODULE')) {
yield $basePath . '/' . $name;
} else {
Expand All @@ -327,6 +370,7 @@ private function findKeysByFileName(string $basePath, string $name) {
$childPath = $basePath . '/' . $child;

// recurse if the child is not a key folder
/** @psalm-suppress InternalMethod */
if ($this->rootView->is_dir($childPath) && !is_dir($childPath . '/OC_DEFAULT_MODULE')) {
yield from $this->findKeysByFileName($childPath, $name);
}
Expand Down Expand Up @@ -363,6 +407,7 @@ private function decryptWithSystemKey(File $node, string $key): void {
$systemKeyPath = $this->getSystemKeyPath($node);
$this->rootView->copy($key, $systemKeyPath);

$decryptedNode = null;
try {
if (!$storage->instanceOfStorage(Encryption::class)) {
$storage = $this->encryptionManager->forceWrapStorage($node->getMountPoint(), $storage);
Expand All @@ -380,19 +425,30 @@ private function decryptWithSystemKey(File $node, string $key): void {
fclose($source);

$decryptedNode->getStorage()->getScanner()->scan($decryptedNode->getInternalPath());
} catch (\Exception $e) {

if ($this->isDataEncrypted($decryptedNode)) {
throw new \Exception($node->getPath() . ' still encrypted after attempting to decrypt with ' . $key);
}
} catch (\Throwable $e) {
// the target has to go first so the .bak can move back onto its name
if ($decryptedNode !== null) {
try {
$decryptedNode->delete();
} catch (\Throwable $cleanupError) {
$this->logger->warning('Failed to remove the partial decryption target ' . $decryptedNode->getPath(), [
'app' => 'encryption',
'exception' => $cleanupError,
]);
}
}
$this->rootView->rmdir($systemKeyPath);

// remove the .bak
// move the backup back onto the original name
$node->move(substr($node->getPath(), 0, -4));

throw $e;
}

if ($this->isDataEncrypted($decryptedNode)) {
throw new \Exception($node->getPath() . ' still encrypted after attempting to decrypt with ' . $key);
}

$this->markAsUnEncrypted($decryptedNode);

$this->rootView->rmdir($systemKeyPath);
Expand Down
Loading
Loading