diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php index 85c5f898f894..5bd548f9fe7f 100644 --- a/apps/files/lib/Command/TransferOwnership.php +++ b/apps/files/lib/Command/TransferOwnership.php @@ -24,10 +24,16 @@ namespace OCA\Files\Command; +use OC\Encryption\CustomEncryptionWrapper; +use OC\Encryption\Manager; +use OC\Files\CustomView; use OC\Files\Filesystem; +use OC\Files\Storage\Wrapper\Wrapper; use OC\Files\View; +use OC\Memcache\ArrayCache; use OCP\Files\FileInfo; use OCP\Files\Mount\IMountManager; +use OCP\ILogger; use OCP\IUserManager; use OCP\Share\IManager; use OCP\Share\IShare; @@ -49,6 +55,12 @@ class TransferOwnership extends Command { /** @var IMountManager */ private $mountManager; + /** @var Manager */ + private $encryptionManager; + + /** @var ILogger */ + private $logger; + /** @var FileInfo[] */ private $allFiles = []; @@ -70,10 +82,12 @@ class TransferOwnership extends Command { /** @var string */ private $finalTarget; - public function __construct(IUserManager $userManager, IManager $shareManager, IMountManager $mountManager) { + public function __construct(IUserManager $userManager, IManager $shareManager, IMountManager $mountManager, Manager $encryptionManager, ILogger $logger) { $this->userManager = $userManager; $this->shareManager = $shareManager; $this->mountManager = $mountManager; + $this->encryptionManager = $encryptionManager; + $this->logger = $logger; parent::__construct(); } @@ -249,7 +263,7 @@ private function collectUsersShares(OutputInterface $output) { * @param OutputInterface $output */ protected function transfer(OutputInterface $output) { - $view = new View(); + $view = new CustomView(); $output->writeln("Transferring files to $this->finalTarget ..."); $sourcePath = (strlen($this->inputPath) > 0) ? $this->inputPath : "$this->sourceUser/files"; // This change will help user to transfer the folder specified using --path option. @@ -260,7 +274,19 @@ protected function transfer(OutputInterface $output) { $this->finalTarget = $this->finalTarget . '/' . basename($sourcePath); } } - $view->rename($sourcePath, $this->finalTarget); + /** + * If encryption is enabled and masterkey is the option selected + * kindly use the CustomView wrapper. + */ + if ($this->encryptionManager->isEnabled() && + \OC::$server->getAppConfig()->getValue('encryption', 'useMasterKey', 0) !== 0) { + $customEncryptionWrapper = new CustomEncryptionWrapper(new ArrayCache(), \OC::$server->getEncryptionManager(), \OC::$server->getLogger()); + Filesystem::addStorageWrapper('oc_customencryption', [$customEncryptionWrapper, 'wrapCustomStorage'], 2); + //A new wrapper for view. + $view->renameCustom($sourcePath, $this->finalTarget); + } else { + $view->rename($sourcePath, $this->finalTarget); + } if (!is_dir("$this->sourceUser/files")) { // because the files folder is moved away we need to recreate it $view->mkdir("$this->sourceUser/files"); diff --git a/lib/private/Encryption/CustomEncryptionWrapper.php b/lib/private/Encryption/CustomEncryptionWrapper.php new file mode 100644 index 000000000000..8b14b30926f7 --- /dev/null +++ b/lib/private/Encryption/CustomEncryptionWrapper.php @@ -0,0 +1,127 @@ + + * @author Joas Schilling + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + + +namespace OC\Encryption; + + +use OC\Files\Storage\Wrapper\CustomEncryption; +use OC\Memcache\ArrayCache; +use OC\Files\Filesystem; +use OC\Files\Storage\Wrapper\Encryption; +use OCP\Files\Mount\IMountPoint; +use OC\Files\View; +use OCP\Files\Storage; +use OCP\ILogger; + +/** + * Class EncryptionWrapper + * + * applies the encryption storage wrapper + * + * @package OC\Encryption + */ +class CustomEncryptionWrapper { + + /** @var ArrayCache */ + private $arrayCache; + + /** @var Manager */ + private $manager; + + /** @var ILogger */ + private $logger; + + /** + * EncryptionWrapper constructor. + * + * @param ArrayCache $arrayCache + * @param Manager $manager + * @param ILogger $logger + */ + public function __construct(ArrayCache $arrayCache, + Manager $manager, + ILogger $logger + ) { + $this->arrayCache = $arrayCache; + $this->manager = $manager; + $this->logger = $logger; + } + + /** + * Wraps the given storage when it is not a shared storage + * + * @param string $mountPoint + * @param Storage $storage + * @param IMountPoint $mount + * @return Encryption|Storage + */ + public function wrapCustomStorage($mountPoint, Storage $storage, IMountPoint $mount) { + $parameters = [ + 'storage' => $storage, + 'mountPoint' => $mountPoint, + 'mount' => $mount + ]; + + if (!$storage->instanceOfStorage('OCA\Files_Sharing\SharedStorage') + && !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') + && !$storage->instanceOfStorage('OC\Files\Storage\OwnCloud')) { + + $user = \OC::$server->getUserSession()->getUser(); + $mountManager = Filesystem::getMountManager(); + $uid = $user ? $user->getUID() : null; + $fileHelper = \OC::$server->getEncryptionFilesHelper(); + $keyStorage = \OC::$server->getEncryptionKeyStorage(); + + $util = new Util( + new View(), + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + \OC::$server->getConfig() + ); + $update = new Update( + new View(), + $util, + Filesystem::getMountManager(), + $this->manager, + $fileHelper, + $uid + ); + + return new CustomEncryption( + $parameters, + $this->manager, + $util, + $this->logger, + $fileHelper, + $uid, + $keyStorage, + $update, + $mountManager, + $this->arrayCache + ); + } else { + return $storage; + } + } + +} diff --git a/lib/private/Files/CustomView.php b/lib/private/Files/CustomView.php new file mode 100644 index 000000000000..3cdfd8854ef8 --- /dev/null +++ b/lib/private/Files/CustomView.php @@ -0,0 +1,289 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Files; + +use OC\Encryption\CustomEncryptionWrapper; +use OC\Encryption\Manager; +use OC\Files\Cache; +use OC\Files\Filesystem; +use OC\Files\Storage\Storage; +use OC\Memcache\ArrayCache; +use OCP\Files\Mount\IMountPoint; +use OCP\Lock\ILockingProvider; +use OC\Files\Mount\MoveableMount; +use OCP\Lock\LockedException; + +class CustomView extends View{ + /** @var string */ + private $fakeRoot = ''; + + private $updaterEnabled = true; + + private $lockingProvider; + + private $lockingEnabled; + + private $encryptionManager; + + private $view; + + /** + * Rename/move a file or folder from the source path to target path. + * + * @param string $path1 source path + * @param string $path2 target path + * + * @return bool|mixed + */ + public function renameCustom($path1, $path2) { + $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1)); + $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2)); + $result = false; + if ( + Filesystem::isValidPath($path2) + and Filesystem::isValidPath($path1) + and !Filesystem::isForbiddenFileOrDir($path2) + ) { + $path1 = $this->getRelativePath($absolutePath1); + $path2 = $this->getRelativePath($absolutePath2); + $exists = $this->file_exists($path2); + + if ($path1 == null or $path2 == null) { + return false; + } + + $this->lockFile($path1, ILockingProvider::LOCK_SHARED, true); + try { + $this->lockFile($path2, ILockingProvider::LOCK_SHARED, true); + } catch (LockedException $e) { + $this->unlockFile($path1, ILockingProvider::LOCK_SHARED); + throw $e; + } + + $run = true; + if ($this->shouldEmitHooks($path1) && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) { + // if it was a rename from a part file to a regular file it was a write and not a rename operation + $this->emit_file_hooks_pre($exists, $path2, $run); + } elseif ($this->shouldEmitHooks($path1)) { + \OC_Hook::emit( + Filesystem::CLASSNAME, Filesystem::signal_rename, + [ + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ] + ); + } + if ($run) { + $this->verifyPath(dirname($path2), basename($path2)); + + $manager = Filesystem::getMountManager(); + $mount1 = $this->getMount($path1); + $mount2 = $this->getMount($path2); + $storage1 = $mount1->getStorage(); + $storage2 = $mount2->getStorage(); + $internalPath1 = $mount1->getInternalPath($absolutePath1); + $internalPath2 = $mount2->getInternalPath($absolutePath2); + + $this->changeLock($path1, ILockingProvider::LOCK_EXCLUSIVE, true); + $this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE, true); + + if ($internalPath1 === '' and $mount1 instanceof MoveableMount) { + if ($this->canMove($mount1, $absolutePath2)) { + /** + * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1 + */ + $sourceMountPoint = $mount1->getMountPoint(); + $result = $mount1->moveMount($absolutePath2); + $manager->moveMount($sourceMountPoint, $mount1->getMountPoint()); + } else { + $result = false; + } + // moving a file/folder within the same mount point + } elseif ($storage1 === $storage2) { + if ($storage1) { + $result = $storage1->rename($internalPath1, $internalPath2); + } else { + $result = false; + } + } else { + $result = $storage2->moveFromStorageCustom($storage1, $internalPath1, $internalPath2, true, true); + } + + if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) { + // if it was a rename from a part file to a regular file it was a write and not a rename operation + + $this->writeUpdate($storage2, $internalPath2); + } else if ($result) { + if ($internalPath1 !== '') { // don't do a cache update for moved mounts + $this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2); + } + } + + $this->changeLock($path1, ILockingProvider::LOCK_SHARED, true); + $this->changeLock($path2, ILockingProvider::LOCK_SHARED, true); + + if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) { + if ($this->shouldEmitHooks()) { + $this->emit_file_hooks_post($exists, $path2); + } + } elseif ($result) { + if ($this->shouldEmitHooks($path1) and $this->shouldEmitHooks($path2)) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_rename, + [ + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2) + ] + ); + } + } + } + $this->unlockFile($path1, ILockingProvider::LOCK_SHARED, true); + $this->unlockFile($path2, ILockingProvider::LOCK_SHARED, true); + } + return $result; + } + + /** + * Copy a file/folder from the source path to target path + * + * @param string $path1 source path + * @param string $path2 target path + * @param bool $preserveMtime whether to preserve mtime on the copy + * @param bool $getDecryptedFile whether to keep a decrypted file + * + * @return bool|mixed + */ + public function copyCustom($path1, $path2, $preserveMtime = false, $getDecryptedFile = false) { + $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1)); + $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2)); + $result = false; + if ( + Filesystem::isValidPath($path2) + and Filesystem::isValidPath($path1) + and !Filesystem::isForbiddenFileOrDir($path2) + ) { + $path1 = $this->getRelativePath($absolutePath1); + $path2 = $this->getRelativePath($absolutePath2); + + if ($path1 == null or $path2 == null) { + return false; + } + $run = true; + + $this->lockFile($path2, ILockingProvider::LOCK_SHARED); + $this->lockFile($path1, ILockingProvider::LOCK_SHARED); + $lockTypePath1 = ILockingProvider::LOCK_SHARED; + $lockTypePath2 = ILockingProvider::LOCK_SHARED; + + try { + + $exists = $this->file_exists($path2); + if ($this->shouldEmitHooks()) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_copy, + [ + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ] + ); + $this->emit_file_hooks_pre($exists, $path2, $run); + } + if ($run) { + $mount1 = $this->getMount($path1); + $mount2 = $this->getMount($path2); + $storage1 = $mount1->getStorage(); + $internalPath1 = $mount1->getInternalPath($absolutePath1); + $storage2 = $mount2->getStorage(); + $internalPath2 = $mount2->getInternalPath($absolutePath2); + + $this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE); + $lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE; + + if ($mount1->getMountPoint() == $mount2->getMountPoint()) { + if ($storage1) { + $result = $storage1->copyCustom($internalPath1, $internalPath2, $getDecryptedFile); + } else { + $result = false; + } + } else { + $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2); + } + + $this->writeUpdate($storage2, $internalPath2); + + $this->changeLock($path2, ILockingProvider::LOCK_SHARED); + $lockTypePath2 = ILockingProvider::LOCK_SHARED; + + if ($this->shouldEmitHooks() && $result !== false) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_copy, + [ + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2) + ] + ); + $this->emit_file_hooks_post($exists, $path2); + } + + } + } catch (\Exception $e) { + $this->unlockFile($path2, $lockTypePath2); + $this->unlockFile($path1, $lockTypePath1); + throw $e; + } + + $this->unlockFile($path2, $lockTypePath2); + $this->unlockFile($path1, $lockTypePath1); + + } + return $result; + } + + private function shouldEmitHooks($path = '') { + if ($path && Cache\Scanner::isPartialFile($path)) { + return false; + } + if (!Filesystem::$loaded) { + return false; + } + $defaultRoot = Filesystem::getRoot(); + if ($defaultRoot === null) { + return false; + } + if ($this->fakeRoot === $defaultRoot) { + return true; + } + $fullPath = $this->getAbsolutePath($path); + + if ($fullPath === $defaultRoot) { + return true; + } + + return (strlen($fullPath) > strlen($defaultRoot)) && (substr($fullPath, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/'); + } +} diff --git a/lib/private/Files/Storage/Wrapper/CustomEncryption.php b/lib/private/Files/Storage/Wrapper/CustomEncryption.php new file mode 100644 index 000000000000..50459220f106 --- /dev/null +++ b/lib/private/Files/Storage/Wrapper/CustomEncryption.php @@ -0,0 +1,631 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Files\Storage\Wrapper; + +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; +use OC\Encryption\Update; +use OC\Encryption\Util; +use OC\Files\Cache\CacheEntry; +use OC\Files\Filesystem; +use OC\Files\Mount\Manager; +use OC\Files\Storage\LocalTempFileTrait; +use OC\Memcache\ArrayCache; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Encryption\IFile; +use OCP\Encryption\IManager; +use OCP\Encryption\Keys\IStorage; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage; +use OCP\ILogger; +use OCP\Files\Cache\ICacheEntry; + +class CustomEncryption extends Wrapper { + + use LocalTempFileTrait; + + /** @var string */ + private $mountPoint; + + /** @var \OC\Encryption\Util */ + private $util; + + /** @var \OCP\Encryption\IManager */ + private $encryptionManager; + + /** @var \OCP\ILogger */ + private $logger; + + /** @var string */ + private $uid; + + /** @var array */ + protected $unencryptedSize; + + /** @var \OCP\Encryption\IFile */ + private $fileHelper; + + /** @var IMountPoint */ + private $mount; + + /** @var IStorage */ + private $keyStorage; + + /** @var Update */ + private $update; + + /** @var Manager */ + private $mountManager; + + /** @var array */ + private $parameters; + + /** @var array remember for which path we execute the repair step to avoid recursions */ + private $fixUnencryptedSizeOf = []; + + /** @var ArrayCache */ + private $arrayCache; + + /** @var Encryption */ + private $encryptionWrapper; + + /** + * @param array $parameters + * @param IManager $encryptionManager + * @param Util $util + * @param ILogger $logger + * @param IFile $fileHelper + * @param string $uid + * @param IStorage $keyStorage + * @param Update $update + * @param Manager $mountManager + * @param ArrayCache $arrayCache + * @param Encryption $encryptionWrapper + */ + public function __construct( + $parameters, + IManager $encryptionManager = null, + Util $util = null, + ILogger $logger = null, + IFile $fileHelper = null, + $uid = null, + IStorage $keyStorage = null, + Update $update = null, + Manager $mountManager = null, + ArrayCache $arrayCache = null + ) { + + $this->parameters = $parameters; + $this->mountPoint = $parameters['mountPoint']; + $this->mount = $parameters['mount']; + $this->encryptionManager = $encryptionManager; + $this->util = $util; + $this->logger = $logger; + $this->uid = $uid; + $this->fileHelper = $fileHelper; + $this->keyStorage = $keyStorage; + $this->unencryptedSize = []; + $this->update = $update; + $this->mountManager = $mountManager; + $this->arrayCache = $arrayCache; + $this->encryptionWrapper = new Encryption( + $parameters, + $this->encryptionManager, + $this->util, + $this->logger, + $this->fileHelper, + $this->uid, + $this->keyStorage, + $this->update, + $this->mountManager, + $this->arrayCache + ); + + parent::__construct($parameters); + } + + public function copyCustom($path1, $path2, $getDecryptedFile = false) { + + $source = $this->getFullPath($path1); + + if ($this->util->isExcluded($source)) { + return $this->storage->copy($path1, $path2); + } + + // need to stream copy file by file in case we copy between a encrypted + // and a unencrypted storage + $this->unlink($path2); + $result = $this->copyFromStorage($this, $path1, $path2, false, false, $getDecryptedFile); + + return $result; + } + + /** + * see http://php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @param string|null $sourceFileOfRename + * @return resource|bool + * @throws GenericEncryptionException + * @throws ModuleDoesNotExistsException + */ + public function fopenCustom($path, $mode, $sourceFileOfRename = null, $getDecryptedFile = false) { + + // check if the file is stored in the array cache, this means that we + // copy a file over to the versions folder, in this case we don't want to + // decrypt it + if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) { + $this->arrayCache->remove('encryption_copy_version_' . $path); + return $this->storage->fopen($path, $mode); + } + + $encryptionEnabled = $this->encryptionManager->isEnabled(); + $shouldEncrypt = false; + $encryptionModule = null; + $header = $this->getHeader($path); + $signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false; + $fullPath = $this->getFullPath($path); + $encryptionModuleId = ($encryptionEnabled) ? $this->util->getEncryptionModuleId($header): ""; + + if ($this->util->isExcluded($fullPath) === false) { + + $size = $unencryptedSize = 0; + $realFile = $this->util->stripPartialFileExtension($path); + $targetExists = $this->file_exists($realFile) || $this->file_exists($path); + $targetIsEncrypted = false; + if ($targetExists) { + // in case the file exists we require the explicit module as + // specified in the file header - otherwise we need to fail hard to + // prevent data loss on client side + if (!empty($encryptionModuleId)) { + $targetIsEncrypted = true; + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + } + + if ($this->file_exists($path)) { + $size = $this->storage->filesize($path); + $unencryptedSize = $this->filesize($path); + } else { + $size = $unencryptedSize = 0; + } + } + + try { + + if ( + $mode === 'w' + || $mode === 'w+' + || $mode === 'wb' + || $mode === 'wb+' + ) { + // don't overwrite encrypted files if encryption is not enabled + if ($targetIsEncrypted && $encryptionEnabled === false) { + throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled'); + } + if ($encryptionEnabled) { + // if $encryptionModuleId is empty, the default module will be used + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); + $signed = true; + } + } else { + $info = $this->getCache()->get($path); + // only get encryption module if we found one in the header + // or if file should be encrypted according to the file cache + if (!empty($encryptionModuleId)) { + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + $shouldEncrypt = true; + } else if (empty($encryptionModuleId) && $info['encrypted'] === true) { + // we come from a old installation. No header and/or no module defined + // but the file is encrypted. In this case we need to use the + // OC_DEFAULT_MODULE to read the file + $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE'); + $shouldEncrypt = true; + $targetIsEncrypted = true; + } + } + } catch (ModuleDoesNotExistsException $e) { + $this->logger->warning('Encryption module "' . $encryptionModuleId . + '" not found, file will be stored unencrypted (' . $e->getMessage() . ')'); + } + + // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt + if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) { + if (!$targetExists || !$targetIsEncrypted) { + $shouldEncrypt = false; + } + } + + if ($shouldEncrypt === true && $encryptionModule !== null) { + /** + * The check of $getDecryptedFile, required to get the file in the decrypted state. + * It will help us get the normal file handler. And hence we can re-encrypt + * the file when necessary, later. The true/false of $getDecryptedFile decides whether + * to keep the file decrypted or not. + */ + if ($getDecryptedFile === true) { + return $this->fopenStorage($path, $mode); + //return $this->storage->fopen($path, $mode); + } + $headerSize = $this->getHeaderSize($path); + $source = $this->fopenStorage($path, $mode); + if (!is_resource($source)) { + return false; + } + + $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header, + $this->uid, $encryptionModule, $this->storage, $this->encryptionWrapper, $this->util, $this->fileHelper, $mode, + $size, $unencryptedSize, $headerSize, $signed, $sourceFileOfRename); + return $handle; + } + + } + + return $this->storage->fopen($path, $mode); + } + + public function fopenStorage($path, $mode) { + return fopen($this->storage->getSourcePath($path), $mode); + } + + /** + * @param Storage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @param bool $preserveMtime + * @return bool + */ + public function moveFromStorageCustom(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true, $isRename = true) { + if ($sourceStorage === $this) { + return $this->rename($sourceInternalPath, $targetInternalPath); + } + + // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed: + // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage + // - copy the file cache update from $this->copyBetweenStorage to this method + // - copy the copyKeys() call from $this->copyBetweenStorage to this method + // - remove $this->copyBetweenStorage + + if (!$sourceStorage->isDeletable($sourceInternalPath)) { + return false; + } + + $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true); + if ($result) { + if ($sourceStorage->is_dir($sourceInternalPath)) { + $result &= $sourceStorage->rmdir($sourceInternalPath); + } else { + $result &= $sourceStorage->unlink($sourceInternalPath); + } + } + return $result; + } + + + /** + * @param Storage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @param bool $preserveMtime + * @param bool $isRename + * @return bool + */ + public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false, $getDecryptedFile = false) { + + // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed: + // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage + // - copy the file cache update from $this->copyBetweenStorage to this method + // - copy the copyKeys() call from $this->copyBetweenStorage to this method + // - remove $this->copyBetweenStorage + + return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename, $getDecryptedFile); + } + + /** + * Update the encrypted cache version in the database + * + * @param Storage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @param bool $isRename + */ + private function updateEncryptedVersion(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) { + $isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0; + $cacheInformation = [ + 'encrypted' => (bool)$isEncrypted, + ]; + if($isEncrypted === 1) { + $encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion']; + + // In case of a move operation from an unencrypted to an encrypted + // storage the old encrypted version would stay with "0" while the + // correct value would be "1". Thus we manually set the value to "1" + // for those cases. + // See also https://github.com/owncloud/core/issues/23078 + if($encryptedVersion === 0) { + $encryptedVersion = 1; + } + + $cacheInformation['encryptedVersion'] = $encryptedVersion; + } + + // in case of a rename we need to manipulate the source cache because + // this information will be kept for the new target + if ($isRename) { + /* + * Rename is a process of creating a new file. Here we try to use the + * incremented version of source file, for the destination file. + */ + $encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion']; + $cacheInformation['encryptedVersion'] = $encryptedVersion + 1; + $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation); + } else { + $this->getCache()->put($targetInternalPath, $cacheInformation); + } + } + + /** + * copy file between two storages + * + * @param Storage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @param bool $preserveMtime + * @param bool $isRename + * @return bool + * @throws \Exception + */ + private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename, $getDecryptedFile = false) { + // for versions we have nothing to do, because versions should always use the + // key from the original file. Just create a 1:1 copy and done + if ($this->isVersion($targetInternalPath) || + $this->isVersion($sourceInternalPath)) { + // remember that we try to create a version so that we can detect it during + // fopen($sourceInternalPath) and by-pass the encryption in order to + // create a 1:1 copy of the file + $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true); + $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath); + if ($result) { + $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath); + // make sure that we update the unencrypted size for the version + if (isset($info['encrypted']) && $info['encrypted'] === true) { + $this->updateUnencryptedSize( + $this->getFullPath($targetInternalPath), + $info['size'] + ); + } + $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename); + } + return $result; + } + + // first copy the keys that we reuse the existing file key on the target location + // and don't create a new one which would break versions for example. + $mount = $this->mountManager->findByStorageId($sourceStorage->getId()); + if (count($mount) === 1) { + $mountPoint = $mount[0]->getMountPoint(); + $source = $mountPoint . '/' . $sourceInternalPath; + $target = $this->getFullPath($targetInternalPath); + $this->copyKeys($source, $target); + } else { + $this->logger->error('Could not find mount point, can\'t keep encryption keys'); + } + + if ($sourceStorage->is_dir($sourceInternalPath)) { + $dh = $sourceStorage->opendir($sourceInternalPath); + $result = $this->mkdir($targetInternalPath); + if (is_resource($dh)) { + while ($result and ($file = readdir($dh)) !== false) { + if (!Filesystem::isIgnoredDir($file)) { + $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename); + } + } + } + } else { + try { + $source = $sourceStorage->fopen($sourceInternalPath, 'r'); + if ($isRename) { + $absSourcePath = Filesystem::normalizePath($sourceStorage->getOwner($sourceInternalPath). '/' . $sourceInternalPath); + $target = $this->fopenCustom($targetInternalPath, 'w', $absSourcePath); + } else { + if ($getDecryptedFile === true) { + $target = $this->fopenCustom($targetInternalPath, 'w', null, $getDecryptedFile); + } else { + $target = $this->fopenCustom($targetInternalPath, 'w'); + } + } + list(, $result) = \OC_Helper::streamCopy($source, $target); + fclose($source); + fclose($target); + } catch (\Exception $e) { + fclose($source); + fclose($target); + throw $e; + } + if($result) { + if ($preserveMtime) { + $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath)); + } + $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename); + } else { + // delete partially written target file + $this->unlink($targetInternalPath); + // delete cache entry that was created by fopen + $this->getCache()->remove($targetInternalPath); + } + } + return (bool)$result; + + } + + /** + * return full path, including mount point + * + * @param string $path relative to mount point + * @return string full path including mount point + */ + protected function getFullPath($path) { + return Filesystem::normalizePath($this->mountPoint . '/' . $path); + } + + /** + * return header size of given file + * + * @param string $path + * @return int + */ + protected function getHeaderSize($path) { + $headerSize = 0; + $realFile = $this->util->stripPartialFileExtension($path); + if ($this->storage->file_exists($realFile)) { + $path = $realFile; + } + $firstBlock = $this->readFirstBlock($path); + + if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) { + $headerSize = $this->util->getHeaderSize(); + } + + return $headerSize; + } + + /** + * parse raw header to array + * + * @param string $rawHeader + * @return array + */ + protected function parseRawHeader($rawHeader) { + $result = []; + if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) { + $header = $rawHeader; + $endAt = strpos($header, Util::HEADER_END); + if ($endAt !== false) { + $header = substr($header, 0, $endAt + strlen(Util::HEADER_END)); + + // +1 to not start with an ':' which would result in empty element at the beginning + $exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1)); + + $element = array_shift($exploded); + while ($element !== Util::HEADER_END) { + $result[$element] = array_shift($exploded); + $element = array_shift($exploded); + } + } + } + + return $result; + } + + /** + * read header from file + * + * @param string $path + * @return array + */ + protected function getHeader($path) { + $realFile = $this->util->stripPartialFileExtension($path); + $exists = $this->storage->file_exists($realFile); + if ($exists) { + $path = $realFile; + } + + $firstBlock = $this->readFirstBlock($path); + $result = $this->parseRawHeader($firstBlock); + + // if the header doesn't contain a encryption module we check if it is a + // legacy file. If true, we add the default encryption module + if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) { + if (!empty($result)) { + $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE'; + } else if ($exists) { + // if the header was empty we have to check first if it is a encrypted file at all + // We would do query to filecache only if we know that entry in filecache exists + $info = $this->getCache()->get($path); + if (isset($info['encrypted']) && $info['encrypted'] === true) { + $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE'; + } + } + } + + return $result; + } + + /** + * read encryption module needed to read/write the file located at $path + * + * @param string $path + * @return null|\OCP\Encryption\IEncryptionModule + * @throws ModuleDoesNotExistsException + * @throws \Exception + */ + protected function getEncryptionModule($path) { + $encryptionModule = null; + $header = $this->getHeader($path); + $encryptionModuleId = $this->util->getEncryptionModuleId($header); + if (!empty($encryptionModuleId)) { + try { + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + } catch (ModuleDoesNotExistsException $e) { + $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!'); + throw $e; + } + } + return $encryptionModule; + } + + /** + * @param string $path + * @param int $unencryptedSize + */ + public function updateUnencryptedSize($path, $unencryptedSize) { + $this->unencryptedSize[$path] = $unencryptedSize; + } + + /** + * copy keys to new location + * + * @param string $source path relative to data/ + * @param string $target path relative to data/ + * @return bool + */ + protected function copyKeys($source, $target) { + if (!$this->util->isExcluded($source)) { + return $this->keyStorage->copyKeys($source, $target); + } + + return false; + } + + /** + * check if path points to a files version + * + * @param $path + * @return bool + */ + protected function isVersion($path) { + $normalized = Filesystem::normalizePath($path); + return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/'; + } + +} diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index b4c270bb8d51..2bfffb815dd3 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -201,16 +201,18 @@ public function getMetaData($path) { */ public function file_get_contents($path) { - $encryptionModule = $this->getEncryptionModule($path); + if ($this->encryptionManager->isEnabled() !== false) { + $encryptionModule = $this->getEncryptionModule($path); - if ($encryptionModule) { - $handle = $this->fopen($path, "r"); - if (!$handle) { - return false; + if ($encryptionModule) { + $handle = $this->fopen($path, "r"); + if (!$handle) { + return false; + } + $data = stream_get_contents($handle); + fclose($handle); + return $data; } - $data = stream_get_contents($handle); - fclose($handle); - return $data; } return $this->storage->file_get_contents($path); } @@ -358,12 +360,11 @@ public function copy($path1, $path2) { * * @param string $path * @param string $mode - * @param string|null $sourceFileOfRename * @return resource|bool * @throws GenericEncryptionException * @throws ModuleDoesNotExistsException */ - public function fopen($path, $mode, $sourceFileOfRename = null) { + public function fopen($path, $mode) { // check if the file is stored in the array cache, this means that we // copy a file over to the versions folder, in this case we don't want to @@ -458,7 +459,7 @@ public function fopen($path, $mode, $sourceFileOfRename = null) { } $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header, $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode, - $size, $unencryptedSize, $headerSize, $signed, $sourceFileOfRename); + $size, $unencryptedSize, $headerSize, $signed); return $handle; } @@ -746,12 +747,7 @@ private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, } else { try { $source = $sourceStorage->fopen($sourceInternalPath, 'r'); - if ($isRename) { - $absSourcePath = Filesystem::normalizePath($sourceStorage->getOwner($sourceInternalPath). '/' . $sourceInternalPath); - $target = $this->fopen($targetInternalPath, 'w', $absSourcePath); - } else { - $target = $this->fopen($targetInternalPath, 'w'); - } + $target = $this->fopen($targetInternalPath, 'w'); list(, $result) = \OC_Helper::streamCopy($source, $target); fclose($source); fclose($target); diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index a13f43a1e2cf..3dfa8d890ccd 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -283,6 +283,10 @@ public function copy($path1, $path2) { return $this->getWrapperStorage()->copy($path1, $path2); } + public function copyCustom($path1, $path2, $getDecryptedFile = false) { + return $this->getWrapperStorage()->copyCustom($path1, $path2, $getDecryptedFile); + } + /** * see http://php.net/manual/en/function.fopen.php *