2015-01-14 20:39:23 +01:00
|
|
|
<?php
|
2024-05-23 09:26:56 +02:00
|
|
|
|
2015-01-14 20:39:23 +01:00
|
|
|
/**
|
2024-05-23 09:26:56 +02:00
|
|
|
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
|
|
|
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
2015-01-14 20:39:23 +01:00
|
|
|
*/
|
|
|
|
namespace OC\Files\Storage\Wrapper;
|
|
|
|
|
|
|
|
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
|
2015-04-23 16:48:11 +02:00
|
|
|
use OC\Encryption\Update;
|
|
|
|
use OC\Encryption\Util;
|
2015-12-02 14:59:13 +01:00
|
|
|
use OC\Files\Cache\CacheEntry;
|
2015-04-20 16:50:12 +02:00
|
|
|
use OC\Files\Filesystem;
|
2015-05-21 14:07:42 +02:00
|
|
|
use OC\Files\Mount\Manager;
|
2022-04-13 16:05:45 +02:00
|
|
|
use OC\Files\ObjectStore\ObjectStoreStorage;
|
2024-08-20 13:32:03 +02:00
|
|
|
use OC\Files\Storage\Common;
|
2015-04-02 14:44:58 +02:00
|
|
|
use OC\Files\Storage\LocalTempFileTrait;
|
2016-03-30 23:20:37 +02:00
|
|
|
use OC\Memcache\ArrayCache;
|
2022-10-21 16:24:32 +02:00
|
|
|
use OCP\Cache\CappedMemoryCache;
|
2015-04-24 13:06:03 +02:00
|
|
|
use OCP\Encryption\IFile;
|
|
|
|
use OCP\Encryption\IManager;
|
2015-04-23 16:48:11 +02:00
|
|
|
use OCP\Encryption\Keys\IStorage;
|
2019-11-22 20:52:10 +01:00
|
|
|
use OCP\Files\Cache\ICacheEntry;
|
2015-04-07 09:42:54 +02:00
|
|
|
use OCP\Files\Mount\IMountPoint;
|
2015-05-21 14:07:42 +02:00
|
|
|
use OCP\Files\Storage;
|
2022-03-17 16:42:53 +01:00
|
|
|
use Psr\Log\LoggerInterface;
|
2015-01-14 20:39:23 +01:00
|
|
|
|
|
|
|
class Encryption extends Wrapper {
|
2015-04-02 14:44:58 +02:00
|
|
|
use LocalTempFileTrait;
|
|
|
|
|
2024-10-01 15:55:36 +02:00
|
|
|
private string $mountPoint;
|
|
|
|
protected array $unencryptedSize = [];
|
|
|
|
private IMountPoint $mount;
|
|
|
|
/** for which path we execute the repair step to avoid recursions */
|
|
|
|
private array $fixUnencryptedSizeOf = [];
|
2022-10-21 16:24:32 +02:00
|
|
|
/** @var CappedMemoryCache<bool> */
|
|
|
|
private CappedMemoryCache $encryptedPaths;
|
2024-10-01 15:55:36 +02:00
|
|
|
private bool $enabled = true;
|
2023-01-10 13:48:31 +01:00
|
|
|
|
2015-01-14 20:39:23 +01:00
|
|
|
/**
|
|
|
|
* @param array $parameters
|
|
|
|
*/
|
|
|
|
public function __construct(
|
2024-10-08 15:13:16 +02:00
|
|
|
array $parameters,
|
2024-10-01 15:55:36 +02:00
|
|
|
private IManager $encryptionManager,
|
|
|
|
private Util $util,
|
|
|
|
private LoggerInterface $logger,
|
|
|
|
private IFile $fileHelper,
|
|
|
|
private ?string $uid,
|
|
|
|
private IStorage $keyStorage,
|
|
|
|
private Update $update,
|
|
|
|
private Manager $mountManager,
|
|
|
|
private ArrayCache $arrayCache,
|
2020-03-27 17:47:20 +01:00
|
|
|
) {
|
2015-01-14 20:39:23 +01:00
|
|
|
$this->mountPoint = $parameters['mountPoint'];
|
2015-04-07 09:42:54 +02:00
|
|
|
$this->mount = $parameters['mount'];
|
2022-10-21 16:24:32 +02:00
|
|
|
$this->encryptedPaths = new CappedMemoryCache();
|
2015-01-14 20:39:23 +01:00
|
|
|
parent::__construct($parameters);
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function filesize(string $path): int|float|false {
|
2015-01-14 20:39:23 +01:00
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
|
|
|
|
$info = $this->getCache()->get($path);
|
2022-10-14 14:50:35 +02:00
|
|
|
if ($info === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-04-01 17:42:56 +02:00
|
|
|
if (isset($this->unencryptedSize[$fullPath])) {
|
2015-01-14 20:39:23 +01:00
|
|
|
$size = $this->unencryptedSize[$fullPath];
|
2023-07-03 09:40:46 +02:00
|
|
|
|
|
|
|
// Update file cache (only if file is already cached).
|
|
|
|
// Certain files are not cached (e.g. *.part).
|
|
|
|
if (isset($info['fileid'])) {
|
|
|
|
if ($info instanceof ICacheEntry) {
|
|
|
|
$info['encrypted'] = $info['encryptedVersion'];
|
|
|
|
} else {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress RedundantCondition
|
|
|
|
*/
|
|
|
|
if (!is_array($info)) {
|
|
|
|
$info = [];
|
|
|
|
}
|
|
|
|
$info['encrypted'] = true;
|
|
|
|
$info = new CacheEntry($info);
|
2016-02-09 18:07:07 +01:00
|
|
|
}
|
2016-02-08 20:57:20 +01:00
|
|
|
|
2023-07-03 09:40:46 +02:00
|
|
|
if ($size !== $info->getUnencryptedSize()) {
|
|
|
|
$this->getCache()->update($info->getId(), [
|
|
|
|
'unencrypted_size' => $size
|
|
|
|
]);
|
|
|
|
}
|
2022-04-13 16:05:45 +02:00
|
|
|
}
|
2015-04-01 17:42:56 +02:00
|
|
|
|
2015-04-14 12:44:51 +02:00
|
|
|
return $size;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2015-04-14 12:44:51 +02:00
|
|
|
if (isset($info['fileid']) && $info['encrypted']) {
|
2022-04-13 16:05:45 +02:00
|
|
|
return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
|
2015-04-14 12:44:51 +02:00
|
|
|
}
|
2016-02-22 17:28:53 +01:00
|
|
|
|
2015-04-14 12:44:51 +02:00
|
|
|
return $this->storage->filesize($path);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2020-03-27 17:47:20 +01:00
|
|
|
private function modifyMetaData(string $path, array $data): array {
|
2015-04-20 14:25:39 +02:00
|
|
|
$fullPath = $this->getFullPath($path);
|
2016-03-31 18:06:37 +02:00
|
|
|
$info = $this->getCache()->get($path);
|
2015-04-20 14:25:39 +02:00
|
|
|
|
|
|
|
if (isset($this->unencryptedSize[$fullPath])) {
|
|
|
|
$data['encrypted'] = true;
|
2015-04-20 16:50:12 +02:00
|
|
|
$data['size'] = $this->unencryptedSize[$fullPath];
|
2023-03-03 17:10:56 +01:00
|
|
|
$data['unencrypted_size'] = $data['size'];
|
2015-04-20 14:25:39 +02:00
|
|
|
} else {
|
|
|
|
if (isset($info['fileid']) && $info['encrypted']) {
|
2022-04-13 16:05:45 +02:00
|
|
|
$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
|
2015-04-20 14:25:39 +02:00
|
|
|
$data['encrypted'] = true;
|
2023-03-03 17:10:56 +01:00
|
|
|
$data['unencrypted_size'] = $data['size'];
|
2015-04-20 14:25:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-31 18:06:37 +02:00
|
|
|
if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
|
|
|
|
$data['encryptedVersion'] = $info['encryptedVersion'];
|
|
|
|
}
|
|
|
|
|
2015-04-20 14:25:39 +02:00
|
|
|
return $data;
|
|
|
|
}
|
2016-03-31 18:06:37 +02:00
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function getMetaData(string $path): ?array {
|
2020-03-27 17:47:20 +01:00
|
|
|
$data = $this->storage->getMetaData($path);
|
|
|
|
if (is_null($data)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $this->modifyMetaData($path, $data);
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function getDirectoryContent(string $directory): \Traversable {
|
2020-03-27 17:47:20 +01:00
|
|
|
$parent = rtrim($directory, '/');
|
|
|
|
foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
|
|
|
|
yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function file_get_contents(string $path): string|false {
|
2015-01-14 20:39:23 +01:00
|
|
|
$encryptionModule = $this->getEncryptionModule($path);
|
|
|
|
|
2015-07-09 18:04:35 +02:00
|
|
|
if ($encryptionModule) {
|
2015-04-02 16:18:10 +02:00
|
|
|
$handle = $this->fopen($path, 'r');
|
|
|
|
if (!$handle) {
|
|
|
|
return false;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
2015-04-02 16:18:10 +02:00
|
|
|
$data = stream_get_contents($handle);
|
|
|
|
fclose($handle);
|
|
|
|
return $data;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
2015-04-02 16:18:10 +02:00
|
|
|
return $this->storage->file_get_contents($path);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function file_put_contents(string $path, mixed $data): int|float|false {
|
2015-01-14 20:39:23 +01:00
|
|
|
// file put content will always be translated to a stream write
|
|
|
|
$handle = $this->fopen($path, 'w');
|
2015-10-15 12:12:52 +02:00
|
|
|
if (is_resource($handle)) {
|
|
|
|
$written = fwrite($handle, $data);
|
|
|
|
fclose($handle);
|
|
|
|
return $written;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function unlink(string $path): bool {
|
2015-04-07 18:05:54 +02:00
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
if ($this->util->isExcluded($fullPath)) {
|
2015-01-14 20:39:23 +01:00
|
|
|
return $this->storage->unlink($path);
|
|
|
|
}
|
|
|
|
|
|
|
|
$encryptionModule = $this->getEncryptionModule($path);
|
|
|
|
if ($encryptionModule) {
|
2020-09-17 09:56:45 +02:00
|
|
|
$this->keyStorage->deleteAllFileKeys($fullPath);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $this->storage->unlink($path);
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function rename(string $source, string $target): bool {
|
2022-10-18 12:49:34 +02:00
|
|
|
$result = $this->storage->rename($source, $target);
|
2015-04-27 11:40:10 +02:00
|
|
|
|
2015-09-29 13:17:39 +02:00
|
|
|
if ($result &&
|
|
|
|
// versions always use the keys from the original file, so we can skip
|
|
|
|
// this step for versions
|
2022-10-18 12:49:34 +02:00
|
|
|
$this->isVersion($target) === false &&
|
2015-09-29 13:17:39 +02:00
|
|
|
$this->encryptionManager->isEnabled()) {
|
2022-10-18 12:49:34 +02:00
|
|
|
$sourcePath = $this->getFullPath($source);
|
|
|
|
if (!$this->util->isExcluded($sourcePath)) {
|
|
|
|
$targetPath = $this->getFullPath($target);
|
|
|
|
if (isset($this->unencryptedSize[$sourcePath])) {
|
|
|
|
$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
|
2015-04-27 11:40:10 +02:00
|
|
|
}
|
2022-10-18 12:49:34 +02:00
|
|
|
$this->keyStorage->renameKeys($sourcePath, $targetPath);
|
|
|
|
$module = $this->getEncryptionModule($target);
|
2016-02-10 12:34:55 +01:00
|
|
|
if ($module) {
|
2022-10-18 12:49:34 +02:00
|
|
|
$module->update($targetPath, $this->uid, []);
|
2016-02-10 12:34:55 +01:00
|
|
|
}
|
2015-04-23 16:48:11 +02:00
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function rmdir(string $path): bool {
|
2015-05-13 14:39:27 +02:00
|
|
|
$result = $this->storage->rmdir($path);
|
2015-05-18 11:54:51 +02:00
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
if ($result &&
|
|
|
|
$this->util->isExcluded($fullPath) === false &&
|
|
|
|
$this->encryptionManager->isEnabled()
|
|
|
|
) {
|
|
|
|
$this->keyStorage->deleteAllFileKeys($fullPath);
|
2015-05-13 14:39:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function isReadable(string $path): bool {
|
2015-05-12 18:49:25 +02:00
|
|
|
$isReadable = true;
|
|
|
|
|
|
|
|
$metaData = $this->getMetaData($path);
|
|
|
|
if (
|
|
|
|
!$this->is_dir($path) &&
|
|
|
|
isset($metaData['encrypted']) &&
|
|
|
|
$metaData['encrypted'] === true
|
|
|
|
) {
|
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
$module = $this->getEncryptionModule($path);
|
|
|
|
$isReadable = $module->isReadable($fullPath, $this->uid);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->storage->isReadable($path) && $isReadable;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function copy(string $source, string $target): bool {
|
2022-10-18 12:49:34 +02:00
|
|
|
$sourcePath = $this->getFullPath($source);
|
2015-04-27 13:13:27 +02:00
|
|
|
|
2022-10-18 12:49:34 +02:00
|
|
|
if ($this->util->isExcluded($sourcePath)) {
|
|
|
|
return $this->storage->copy($source, $target);
|
2015-04-02 16:25:01 +02:00
|
|
|
}
|
|
|
|
|
2015-08-24 15:57:03 +02:00
|
|
|
// need to stream copy file by file in case we copy between a encrypted
|
|
|
|
// and a unencrypted storage
|
2022-10-18 12:49:34 +02:00
|
|
|
$this->unlink($target);
|
|
|
|
return $this->copyFromStorage($this, $source, $target);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function fopen(string $path, string $mode) {
|
2016-03-30 23:20:37 +02:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2023-01-10 13:48:31 +01:00
|
|
|
if (!$this->enabled) {
|
|
|
|
return $this->storage->fopen($path, $mode);
|
|
|
|
}
|
|
|
|
|
2015-04-08 16:41:20 +02:00
|
|
|
$encryptionEnabled = $this->encryptionManager->isEnabled();
|
2015-01-14 20:39:23 +01:00
|
|
|
$shouldEncrypt = false;
|
|
|
|
$encryptionModule = null;
|
2015-07-09 18:04:35 +02:00
|
|
|
$header = $this->getHeader($path);
|
2018-01-26 12:36:25 +01:00
|
|
|
$signed = isset($header['signed']) && $header['signed'] === 'true';
|
2015-01-14 20:39:23 +01:00
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
$encryptionModuleId = $this->util->getEncryptionModuleId($header);
|
|
|
|
|
2015-05-27 13:01:32 +02:00
|
|
|
if ($this->util->isExcluded($fullPath) === false) {
|
|
|
|
$size = $unencryptedSize = 0;
|
2015-06-23 10:43:28 +02:00
|
|
|
$realFile = $this->util->stripPartialFileExtension($path);
|
2021-06-09 14:38:21 +02:00
|
|
|
$targetExists = $this->is_file($realFile) || $this->file_exists($path);
|
2015-05-27 13:01:32 +02:00
|
|
|
$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);
|
|
|
|
}
|
2015-04-02 17:16:27 +02:00
|
|
|
|
2015-06-30 10:21:03 +02:00
|
|
|
if ($this->file_exists($path)) {
|
|
|
|
$size = $this->storage->filesize($path);
|
|
|
|
$unencryptedSize = $this->filesize($path);
|
|
|
|
} else {
|
|
|
|
$size = $unencryptedSize = 0;
|
|
|
|
}
|
2015-05-27 13:01:32 +02:00
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
|
2015-05-27 13:01:32 +02:00
|
|
|
try {
|
|
|
|
if (
|
|
|
|
$mode === 'w'
|
|
|
|
|| $mode === 'w+'
|
|
|
|
|| $mode === 'wb'
|
|
|
|
|| $mode === 'wb+'
|
|
|
|
) {
|
2018-07-05 16:57:48 +02:00
|
|
|
// if we update a encrypted file with a un-encrypted one we change the db flag
|
2015-07-09 18:04:35 +02:00
|
|
|
if ($targetIsEncrypted && $encryptionEnabled === false) {
|
2018-07-05 16:57:48 +02:00
|
|
|
$cache = $this->storage->getCache();
|
2024-09-19 18:19:34 +02:00
|
|
|
$entry = $cache->get($path);
|
|
|
|
$cache->update($entry->getId(), ['encrypted' => 0]);
|
2015-07-09 18:04:35 +02:00
|
|
|
}
|
2015-05-27 13:01:32 +02:00
|
|
|
if ($encryptionEnabled) {
|
|
|
|
// if $encryptionModuleId is empty, the default module will be used
|
|
|
|
$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
|
|
|
|
$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
|
2016-01-05 15:29:44 +01:00
|
|
|
$signed = true;
|
2015-05-27 13:01:32 +02:00
|
|
|
}
|
|
|
|
} 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;
|
2020-04-10 10:35:09 +02:00
|
|
|
} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
|
2015-05-27 13:01:32 +02:00
|
|
|
// 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;
|
2015-07-10 13:14:07 +02:00
|
|
|
$targetIsEncrypted = true;
|
2015-05-27 13:01:32 +02:00
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
2015-05-27 13:01:32 +02:00
|
|
|
} catch (ModuleDoesNotExistsException $e) {
|
2022-03-17 16:42:53 +01:00
|
|
|
$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
|
|
|
|
'exception' => $e,
|
2018-01-17 15:21:56 +01:00
|
|
|
'app' => 'core',
|
|
|
|
]);
|
2015-05-27 13:01:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
|
2016-11-29 16:27:28 +01:00
|
|
|
if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
|
2015-05-27 13:01:32 +02:00
|
|
|
if (!$targetExists || !$targetIsEncrypted) {
|
|
|
|
$shouldEncrypt = false;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-27 13:01:32 +02:00
|
|
|
if ($shouldEncrypt === true && $encryptionModule !== null) {
|
2022-10-21 16:24:32 +02:00
|
|
|
$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
|
2015-07-27 14:29:07 +02:00
|
|
|
$headerSize = $this->getHeaderSize($path);
|
2015-05-27 13:01:32 +02:00
|
|
|
$source = $this->storage->fopen($path, $mode);
|
2015-10-15 12:06:49 +02:00
|
|
|
if (!is_resource($source)) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-05-27 13:01:32 +02:00
|
|
|
$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
|
|
|
|
$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
|
2016-01-05 15:29:44 +01:00
|
|
|
$size, $unencryptedSize, $headerSize, $signed);
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
|
2015-05-27 13:01:32 +02:00
|
|
|
return $handle;
|
2015-04-02 17:16:27 +02:00
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
2015-05-27 13:01:32 +02:00
|
|
|
|
|
|
|
return $this->storage->fopen($path, $mode);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2016-02-22 17:28:53 +01:00
|
|
|
|
|
|
|
/**
|
2024-07-29 14:43:27 +08:00
|
|
|
* perform some plausibility checks if the unencrypted size is correct.
|
2016-02-22 17:28:53 +01:00
|
|
|
* If not, we calculate the correct unencrypted size and return it
|
|
|
|
*
|
|
|
|
* @param string $path internal path relative to the storage root
|
|
|
|
* @param int $unencryptedSize size of the unencrypted file
|
|
|
|
*
|
|
|
|
* @return int unencrypted size
|
|
|
|
*/
|
2022-04-13 16:05:45 +02:00
|
|
|
protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
|
2016-02-22 17:28:53 +01:00
|
|
|
$size = $this->storage->filesize($path);
|
|
|
|
$result = $unencryptedSize;
|
|
|
|
|
|
|
|
if ($unencryptedSize < 0 ||
|
2023-03-03 17:10:56 +01:00
|
|
|
($size > 0 && $unencryptedSize === $size) ||
|
|
|
|
$unencryptedSize > $size
|
2016-02-22 17:28:53 +01:00
|
|
|
) {
|
|
|
|
// check if we already calculate the unencrypted size for the
|
|
|
|
// given path to avoid recursions
|
|
|
|
if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
|
|
|
|
$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
|
|
|
|
try {
|
|
|
|
$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
|
|
|
|
} catch (\Exception $e) {
|
2022-03-17 16:42:53 +01:00
|
|
|
$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
|
2016-02-22 17:28:53 +01:00
|
|
|
}
|
|
|
|
unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* calculate the unencrypted size
|
|
|
|
*
|
|
|
|
* @param string $path internal path relative to the storage root
|
|
|
|
* @param int $size size of the physical file
|
|
|
|
* @param int $unencryptedSize size of the unencrypted file
|
|
|
|
*/
|
2024-09-19 18:19:34 +02:00
|
|
|
protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int|float {
|
2016-02-22 17:28:53 +01:00
|
|
|
$headerSize = $this->getHeaderSize($path);
|
|
|
|
$header = $this->getHeader($path);
|
|
|
|
$encryptionModule = $this->getEncryptionModule($path);
|
|
|
|
|
|
|
|
$stream = $this->storage->fopen($path, 'r');
|
|
|
|
|
|
|
|
// if we couldn't open the file we return the old unencrypted size
|
|
|
|
if (!is_resource($stream)) {
|
|
|
|
$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
|
|
|
|
return $unencryptedSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
$newUnencryptedSize = 0;
|
|
|
|
$size -= $headerSize;
|
|
|
|
$blockSize = $this->util->getBlockSize();
|
|
|
|
|
|
|
|
// if a header exists we skip it
|
|
|
|
if ($headerSize > 0) {
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
$this->fread_block($stream, $headerSize);
|
2016-02-22 17:28:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// fast path, else the calculation for $lastChunkNr is bogus
|
|
|
|
if ($size === 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-26 12:36:25 +01:00
|
|
|
$signed = isset($header['signed']) && $header['signed'] === 'true';
|
2016-02-22 17:28:53 +01:00
|
|
|
$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
|
|
|
|
|
|
|
|
// calculate last chunk nr
|
|
|
|
// next highest is end of chunks, one subtracted is last one
|
|
|
|
// we have to read the last chunk, we can't just calculate it (because of padding etc)
|
|
|
|
|
2020-03-27 17:47:20 +01:00
|
|
|
$lastChunkNr = ceil($size / $blockSize) - 1;
|
2016-02-22 17:28:53 +01:00
|
|
|
// calculate last chunk position
|
|
|
|
$lastChunkPos = ($lastChunkNr * $blockSize);
|
|
|
|
// try to fseek to the last chunk, if it fails we have to read the whole file
|
|
|
|
if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
|
|
|
|
$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
|
|
|
|
}
|
|
|
|
|
2020-03-27 17:47:20 +01:00
|
|
|
$lastChunkContentEncrypted = '';
|
2016-02-22 17:28:53 +01:00
|
|
|
$count = $blockSize;
|
|
|
|
|
|
|
|
while ($count > 0) {
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
$data = $this->fread_block($stream, $blockSize);
|
2020-03-27 17:47:20 +01:00
|
|
|
$count = strlen($data);
|
2016-02-22 17:28:53 +01:00
|
|
|
$lastChunkContentEncrypted .= $data;
|
|
|
|
if (strlen($lastChunkContentEncrypted) > $blockSize) {
|
|
|
|
$newUnencryptedSize += $unencryptedBlockSize;
|
2020-03-27 17:47:20 +01:00
|
|
|
$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
|
2016-02-22 17:28:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose($stream);
|
|
|
|
|
|
|
|
// we have to decrypt the last chunk to get it actual size
|
|
|
|
$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
|
|
|
|
$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
|
|
|
|
$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
|
|
|
|
|
|
|
|
// calc the real file size with the size of the last chunk
|
|
|
|
$newUnencryptedSize += strlen($decryptedLastChunk);
|
|
|
|
|
|
|
|
$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
|
|
|
|
|
|
|
|
// write to cache if applicable
|
|
|
|
$cache = $this->storage->getCache();
|
2024-09-19 18:19:34 +02:00
|
|
|
$entry = $cache->get($path);
|
|
|
|
$cache->update($entry['fileid'], [
|
|
|
|
'unencrypted_size' => $newUnencryptedSize
|
|
|
|
]);
|
2016-02-22 17:28:53 +01:00
|
|
|
|
|
|
|
return $newUnencryptedSize;
|
|
|
|
}
|
|
|
|
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
/**
|
|
|
|
* fread_block
|
|
|
|
*
|
|
|
|
* This function is a wrapper around the fread function. It is based on the
|
|
|
|
* stream_read_block function from lib/private/Files/Streams/Encryption.php
|
|
|
|
* It calls stream read until the requested $blockSize was received or no remaining data is present.
|
|
|
|
* This is required as stream_read only returns smaller chunks of data when the stream fetches from a
|
|
|
|
* remote storage over the internet and it does not care about the given $blockSize.
|
|
|
|
*
|
2024-10-01 16:12:30 +02:00
|
|
|
* @param resource $handle the stream to read from
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
* @param int $blockSize Length of requested data block in bytes
|
|
|
|
* @return string Data fetched from stream.
|
|
|
|
*/
|
2021-08-30 13:31:54 +01:00
|
|
|
private function fread_block($handle, int $blockSize): string {
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
$remaining = $blockSize;
|
|
|
|
$data = '';
|
|
|
|
|
|
|
|
do {
|
|
|
|
$chunk = fread($handle, $remaining);
|
|
|
|
$chunk_len = strlen($chunk);
|
|
|
|
$data .= $chunk;
|
|
|
|
$remaining -= $chunk_len;
|
|
|
|
} while (($remaining > 0) && ($chunk_len > 0));
|
|
|
|
|
2021-08-30 13:31:54 +01:00
|
|
|
return $data;
|
Fix truncation of files upon read when using object store and encryption.
When using and object store as primary storage and using the default
encryption module at the same time, any encrypted file would be truncated
when read, and a text error message added to the end.
This was caused by a combination of the reliance of the read functions on
on knowing the unencrypted file size, and a bug in the function which
calculated the unencrypted file size for a given file.
In order to calculate the unencrypted file size, the function would first
skip the header block, then use fseek to skip to the last encrypted block
in the file. Because there was a corresponence between the encrypted and
unencrypted blocks, this would also be the last encrypted block. It would
then read the final block and decrypt it to get the unencrypted length of
the last block. With that, the number of blocks, and the unencrypted block
size, it could calculate the unencrypted file size.
The trouble was that when using an object store, an fread call doesn't
always get you the number of bytes you asked for, even if they are
available. To resolve this I adapted the stream_read_block function from
lib/private/Files/Streams/Encryption.php to work here. This function
wraps the fread call in a loop and repeats until it has the entire set of
bytes that were requested, or there are no more to get.
This fixes the imediate bug, and should (with luck) allow people to get
their encrypted files out of Nextcloud now. (The problem was purely on
the decryption side). In the future it would be nice to do some
refactoring here.
I have tested this with image files ranging from 1kb to 10mb using
Nextcloud version 22.1.0 (the nextcloud:22.1-apache docker image), with
sqlite and a Linode object store as the primary storage.
Signed-off-by: Alan Meeson <alan@carefullycalculated.co.uk>
2021-08-11 19:34:23 +01:00
|
|
|
}
|
|
|
|
|
2022-04-13 16:05:45 +02:00
|
|
|
public function moveFromStorage(
|
|
|
|
Storage\IStorage $sourceStorage,
|
2024-10-01 16:12:30 +02:00
|
|
|
string $sourceInternalPath,
|
|
|
|
string $targetInternalPath,
|
2022-04-13 16:05:45 +02:00
|
|
|
$preserveMtime = true,
|
2024-09-19 18:19:34 +02:00
|
|
|
): bool {
|
2015-09-07 17:19:50 +02:00
|
|
|
if ($sourceStorage === $this) {
|
|
|
|
return $this->rename($sourceInternalPath, $targetInternalPath);
|
|
|
|
}
|
2015-04-30 14:30:02 +02:00
|
|
|
|
|
|
|
// 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
|
2015-05-21 14:07:42 +02:00
|
|
|
// - copy the copyKeys() call from $this->copyBetweenStorage to this method
|
2015-04-30 14:30:02 +02:00
|
|
|
// - remove $this->copyBetweenStorage
|
|
|
|
|
2016-02-19 12:59:10 +01:00
|
|
|
if (!$sourceStorage->isDeletable($sourceInternalPath)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-04-30 14:30:02 +02:00
|
|
|
$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
|
2015-04-15 09:49:50 +02:00
|
|
|
if ($result) {
|
|
|
|
if ($sourceStorage->is_dir($sourceInternalPath)) {
|
2024-09-19 18:19:34 +02:00
|
|
|
$result = $sourceStorage->rmdir($sourceInternalPath);
|
2015-04-15 09:49:50 +02:00
|
|
|
} else {
|
2024-09-19 18:19:34 +02:00
|
|
|
$result = $sourceStorage->unlink($sourceInternalPath);
|
2015-04-15 09:49:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2022-04-13 16:05:45 +02:00
|
|
|
public function copyFromStorage(
|
|
|
|
Storage\IStorage $sourceStorage,
|
2024-10-01 16:12:30 +02:00
|
|
|
string $sourceInternalPath,
|
|
|
|
string $targetInternalPath,
|
2022-04-13 16:05:45 +02:00
|
|
|
$preserveMtime = false,
|
|
|
|
$isRename = false,
|
2024-09-19 18:19:34 +02:00
|
|
|
): bool {
|
2015-04-30 14:30:02 +02:00
|
|
|
// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
|
2015-09-29 13:17:39 +02:00
|
|
|
// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
|
2015-04-30 14:30:02 +02:00
|
|
|
// - copy the file cache update from $this->copyBetweenStorage to this method
|
2015-05-21 14:07:42 +02:00
|
|
|
// - copy the copyKeys() call from $this->copyBetweenStorage to this method
|
2015-04-30 14:30:02 +02:00
|
|
|
// - remove $this->copyBetweenStorage
|
|
|
|
|
2016-04-20 12:12:41 +02:00
|
|
|
return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
|
2015-04-30 14:30:02 +02:00
|
|
|
}
|
|
|
|
|
2016-03-02 20:37:13 +01:00
|
|
|
/**
|
|
|
|
* Update the encrypted cache version in the database
|
|
|
|
*/
|
2022-04-13 16:05:45 +02:00
|
|
|
private function updateEncryptedVersion(
|
|
|
|
Storage\IStorage $sourceStorage,
|
2024-10-01 16:12:30 +02:00
|
|
|
string $sourceInternalPath,
|
|
|
|
string $targetInternalPath,
|
|
|
|
bool $isRename,
|
|
|
|
bool $keepEncryptionVersion,
|
2024-09-19 18:19:34 +02:00
|
|
|
): void {
|
2018-03-28 16:27:29 +02:00
|
|
|
$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
|
2016-03-02 20:37:13 +01:00
|
|
|
$cacheInformation = [
|
2018-03-28 16:27:29 +02:00
|
|
|
'encrypted' => $isEncrypted,
|
2016-03-02 20:37:13 +01:00
|
|
|
];
|
2018-03-28 16:27:29 +02:00
|
|
|
if ($isEncrypted) {
|
2022-01-12 15:28:34 +01:00
|
|
|
$sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
|
|
|
|
$targetCacheEntry = $this->getCache()->get($targetInternalPath);
|
|
|
|
|
|
|
|
// Rename of the cache already happened, so we do the cleanup on the target
|
|
|
|
if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
|
|
|
|
$encryptedVersion = $targetCacheEntry['encryptedVersion'];
|
|
|
|
$isRename = false;
|
|
|
|
} else {
|
|
|
|
$encryptedVersion = $sourceCacheEntry['encryptedVersion'];
|
|
|
|
}
|
2016-03-10 15:58:24 +01:00
|
|
|
|
|
|
|
// 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
|
2018-03-28 16:27:29 +02:00
|
|
|
if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
|
2016-03-10 15:58:24 +01:00
|
|
|
$encryptedVersion = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
$cacheInformation['encryptedVersion'] = $encryptedVersion;
|
2016-03-02 20:37:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// in case of a rename we need to manipulate the source cache because
|
|
|
|
// this information will be kept for the new target
|
|
|
|
if ($isRename) {
|
|
|
|
$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
|
|
|
|
} else {
|
|
|
|
$this->getCache()->put($targetInternalPath, $cacheInformation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-30 14:30:02 +02:00
|
|
|
/**
|
|
|
|
* copy file between two storages
|
2015-11-04 10:15:28 +01:00
|
|
|
* @throws \Exception
|
2015-04-30 14:30:02 +02:00
|
|
|
*/
|
2022-04-13 16:05:45 +02:00
|
|
|
private function copyBetweenStorage(
|
|
|
|
Storage\IStorage $sourceStorage,
|
2024-10-01 16:12:30 +02:00
|
|
|
string $sourceInternalPath,
|
|
|
|
string $targetInternalPath,
|
|
|
|
bool $preserveMtime,
|
|
|
|
bool $isRename,
|
2024-09-19 18:19:34 +02:00
|
|
|
): bool {
|
2015-09-29 13:17:39 +02:00
|
|
|
// 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)) {
|
2016-03-30 23:20:37 +02:00
|
|
|
// 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);
|
2015-11-04 10:15:28 +01:00
|
|
|
$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
|
2016-03-31 21:52:23 +02:00
|
|
|
$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
|
2015-11-04 10:15:28 +01:00
|
|
|
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),
|
2022-04-13 16:05:45 +02:00
|
|
|
$info->getUnencryptedSize()
|
2015-11-04 10:15:28 +01:00
|
|
|
);
|
|
|
|
}
|
2024-12-17 19:55:13 +01:00
|
|
|
$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
|
2015-11-04 10:15:28 +01:00
|
|
|
}
|
|
|
|
return $result;
|
2015-09-29 13:17:39 +02:00
|
|
|
}
|
|
|
|
|
2015-05-21 14:07:42 +02:00
|
|
|
// 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.
|
2024-08-20 13:32:03 +02:00
|
|
|
if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) {
|
|
|
|
$mountPoint = $sourceStorage->getMountOption('mount_point');
|
2015-05-21 14:07:42 +02:00
|
|
|
$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');
|
|
|
|
}
|
|
|
|
|
2015-04-15 09:49:50 +02:00
|
|
|
if ($sourceStorage->is_dir($sourceInternalPath)) {
|
|
|
|
$dh = $sourceStorage->opendir($sourceInternalPath);
|
2022-01-10 11:15:22 +01:00
|
|
|
if (!$this->is_dir($targetInternalPath)) {
|
|
|
|
$result = $this->mkdir($targetInternalPath);
|
|
|
|
} else {
|
|
|
|
$result = true;
|
|
|
|
}
|
2015-04-15 09:49:50 +02:00
|
|
|
if (is_resource($dh)) {
|
2024-11-21 08:38:50 +01:00
|
|
|
while ($result && ($file = readdir($dh)) !== false) {
|
2015-04-15 09:49:50 +02:00
|
|
|
if (!Filesystem::isIgnoredDir($file)) {
|
2016-04-20 12:12:41 +02:00
|
|
|
$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
|
2015-04-15 09:49:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2015-08-24 15:57:03 +02:00
|
|
|
try {
|
|
|
|
$source = $sourceStorage->fopen($sourceInternalPath, 'r');
|
|
|
|
$target = $this->fopen($targetInternalPath, 'w');
|
2020-03-27 17:47:20 +01:00
|
|
|
[, $result] = \OC_Helper::streamCopy($source, $target);
|
2023-01-30 10:07:01 +01:00
|
|
|
} finally {
|
2021-12-09 11:28:10 +01:00
|
|
|
if (is_resource($source)) {
|
|
|
|
fclose($source);
|
|
|
|
}
|
|
|
|
if (is_resource($target)) {
|
|
|
|
fclose($target);
|
|
|
|
}
|
2015-08-24 15:57:03 +02:00
|
|
|
}
|
2015-04-30 14:30:02 +02:00
|
|
|
if ($result) {
|
|
|
|
if ($preserveMtime) {
|
|
|
|
$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
|
|
|
|
}
|
2018-03-28 16:27:29 +02:00
|
|
|
$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
|
2015-04-30 14:30:02 +02:00
|
|
|
} else {
|
2015-04-15 09:49:50 +02:00
|
|
|
// delete partially written target file
|
|
|
|
$this->unlink($targetInternalPath);
|
|
|
|
// delete cache entry that was created by fopen
|
|
|
|
$this->getCache()->remove($targetInternalPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (bool)$result;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function getLocalFile(string $path): string|false {
|
2015-05-05 16:19:24 +02:00
|
|
|
if ($this->encryptionManager->isEnabled()) {
|
2015-05-06 11:16:44 +02:00
|
|
|
$cachedFile = $this->getCachedFile($path);
|
|
|
|
if (is_string($cachedFile)) {
|
|
|
|
return $cachedFile;
|
|
|
|
}
|
2015-05-05 16:19:24 +02:00
|
|
|
}
|
|
|
|
return $this->storage->getLocalFile($path);
|
2015-04-02 14:44:58 +02:00
|
|
|
}
|
|
|
|
|
2024-09-19 18:19:34 +02:00
|
|
|
public function isLocal(): bool {
|
2015-04-27 14:26:05 +02:00
|
|
|
if ($this->encryptionManager->isEnabled()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return $this->storage->isLocal();
|
2015-04-02 14:44:58 +02:00
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function stat(string $path): array|false {
|
2015-04-02 14:44:58 +02:00
|
|
|
$stat = $this->storage->stat($path);
|
2022-04-13 16:05:45 +02:00
|
|
|
if (!$stat) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-04-02 14:44:58 +02:00
|
|
|
$fileSize = $this->filesize($path);
|
|
|
|
$stat['size'] = $fileSize;
|
|
|
|
$stat[7] = $fileSize;
|
2020-08-17 21:00:37 +02:00
|
|
|
$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
|
2015-04-02 14:44:58 +02:00
|
|
|
return $stat;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function hash(string $type, string $path, bool $raw = false): string|false {
|
2015-04-02 14:44:58 +02:00
|
|
|
$fh = $this->fopen($path, 'rb');
|
|
|
|
$ctx = hash_init($type);
|
|
|
|
hash_update_stream($ctx, $fh);
|
|
|
|
fclose($fh);
|
|
|
|
return hash_final($ctx, $raw);
|
|
|
|
}
|
|
|
|
|
2015-01-14 20:39:23 +01:00
|
|
|
/**
|
|
|
|
* return full path, including mount point
|
|
|
|
*
|
|
|
|
* @param string $path relative to mount point
|
|
|
|
* @return string full path including mount point
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function getFullPath(string $path): string {
|
2015-04-20 16:50:12 +02:00
|
|
|
return Filesystem::normalizePath($this->mountPoint . '/' . $path);
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
2015-07-09 18:04:35 +02:00
|
|
|
/**
|
|
|
|
* read first block of encrypted file, typically this will contain the
|
|
|
|
* encryption header
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function readFirstBlock(string $path): string {
|
2015-07-09 18:04:35 +02:00
|
|
|
$firstBlock = '';
|
2021-06-09 14:38:21 +02:00
|
|
|
if ($this->storage->is_file($path)) {
|
2015-07-09 18:04:35 +02:00
|
|
|
$handle = $this->storage->fopen($path, 'r');
|
|
|
|
$firstBlock = fread($handle, $this->util->getHeaderSize());
|
|
|
|
fclose($handle);
|
|
|
|
}
|
|
|
|
return $firstBlock;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* return header size of given file
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function getHeaderSize(string $path): int {
|
2015-07-09 18:04:35 +02:00
|
|
|
$headerSize = 0;
|
|
|
|
$realFile = $this->util->stripPartialFileExtension($path);
|
2021-06-09 14:38:21 +02:00
|
|
|
if ($this->storage->is_file($realFile)) {
|
2015-07-09 18:04:35 +02:00
|
|
|
$path = $realFile;
|
|
|
|
}
|
|
|
|
$firstBlock = $this->readFirstBlock($path);
|
|
|
|
|
2023-05-15 15:17:19 +03:30
|
|
|
if (str_starts_with($firstBlock, Util::HEADER_START)) {
|
2016-03-21 15:47:24 +01:00
|
|
|
$headerSize = $this->util->getHeaderSize();
|
2015-07-09 18:04:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $headerSize;
|
|
|
|
}
|
|
|
|
|
2015-01-14 20:39:23 +01:00
|
|
|
/**
|
|
|
|
* read header from file
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function getHeader(string $path): array {
|
2015-06-23 10:43:28 +02:00
|
|
|
$realFile = $this->util->stripPartialFileExtension($path);
|
2021-06-09 14:38:21 +02:00
|
|
|
$exists = $this->storage->is_file($realFile);
|
2017-03-28 14:49:06 +02:00
|
|
|
if ($exists) {
|
2015-06-26 11:26:40 +02:00
|
|
|
$path = $realFile;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:14:49 +01:00
|
|
|
$result = [];
|
2021-09-21 17:40:19 +02:00
|
|
|
|
2022-10-21 16:24:32 +02:00
|
|
|
$isEncrypted = $this->encryptedPaths->get($realFile);
|
|
|
|
if (is_null($isEncrypted)) {
|
|
|
|
$info = $this->getCache()->get($path);
|
|
|
|
$isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
|
|
|
|
}
|
2021-09-21 17:40:19 +02:00
|
|
|
|
2022-10-21 16:24:32 +02:00
|
|
|
if ($isEncrypted) {
|
2021-01-05 11:14:49 +01:00
|
|
|
$firstBlock = $this->readFirstBlock($path);
|
2023-01-10 13:48:31 +01:00
|
|
|
$result = $this->util->parseRawHeader($firstBlock);
|
2015-07-09 18:04:35 +02:00
|
|
|
|
2021-01-05 11:14:49 +01:00
|
|
|
// 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
|
2021-01-16 14:49:53 +01:00
|
|
|
if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
|
2015-07-09 18:04:35 +02:00
|
|
|
$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
|
2015-04-24 13:02:06 +02:00
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
2015-07-09 18:04:35 +02:00
|
|
|
|
|
|
|
return $result;
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* read encryption module needed to read/write the file located at $path
|
|
|
|
*
|
2015-03-30 13:23:10 +02:00
|
|
|
* @throws ModuleDoesNotExistsException
|
|
|
|
* @throws \Exception
|
2015-01-14 20:39:23 +01:00
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function getEncryptionModule(string $path): ?\OCP\Encryption\IEncryptionModule {
|
2015-01-14 20:39:23 +01:00
|
|
|
$encryptionModule = null;
|
2015-07-09 18:04:35 +02:00
|
|
|
$header = $this->getHeader($path);
|
2015-01-14 20:39:23 +01:00
|
|
|
$encryptionModuleId = $this->util->getEncryptionModuleId($header);
|
|
|
|
if (!empty($encryptionModuleId)) {
|
|
|
|
try {
|
|
|
|
$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
|
|
|
|
} catch (ModuleDoesNotExistsException $e) {
|
2015-03-30 13:23:10 +02:00
|
|
|
$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
|
2015-01-14 20:39:23 +01:00
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
}
|
2016-11-29 16:27:28 +01:00
|
|
|
|
2015-01-14 20:39:23 +01:00
|
|
|
return $encryptionModule;
|
|
|
|
}
|
|
|
|
|
2024-10-01 16:12:30 +02:00
|
|
|
public function updateUnencryptedSize(string $path, int|float $unencryptedSize): void {
|
2015-01-14 20:39:23 +01:00
|
|
|
$this->unencryptedSize[$path] = $unencryptedSize;
|
|
|
|
}
|
2015-05-21 14:07:42 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* copy keys to new location
|
|
|
|
*
|
|
|
|
* @param string $source path relative to data/
|
|
|
|
* @param string $target path relative to data/
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function copyKeys(string $source, string $target): bool {
|
2015-05-21 14:07:42 +02:00
|
|
|
if (!$this->util->isExcluded($source)) {
|
|
|
|
return $this->keyStorage->copyKeys($source, $target);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2015-07-09 18:04:35 +02:00
|
|
|
|
2015-09-29 13:17:39 +02:00
|
|
|
/**
|
|
|
|
* check if path points to a files version
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function isVersion(string $path): bool {
|
2015-09-29 13:17:39 +02:00
|
|
|
$normalized = Filesystem::normalizePath($path);
|
|
|
|
return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
|
|
|
|
}
|
|
|
|
|
2016-11-29 16:27:28 +01:00
|
|
|
/**
|
|
|
|
* check if the given storage should be encrypted or not
|
|
|
|
*/
|
2024-10-01 16:12:30 +02:00
|
|
|
protected function shouldEncrypt(string $path): bool {
|
2016-11-29 16:27:28 +01:00
|
|
|
$fullPath = $this->getFullPath($path);
|
|
|
|
$mountPointConfig = $this->mount->getOption('encrypt', true);
|
|
|
|
if ($mountPointConfig === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$encryptionModule = $this->getEncryptionModule($fullPath);
|
|
|
|
} catch (ModuleDoesNotExistsException $e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($encryptionModule === null) {
|
|
|
|
$encryptionModule = $this->encryptionManager->getEncryptionModule();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $encryptionModule->shouldEncrypt($fullPath);
|
|
|
|
}
|
|
|
|
|
2024-03-28 16:13:19 +01:00
|
|
|
public function writeStream(string $path, $stream, ?int $size = null): int {
|
2018-10-31 16:16:37 +01:00
|
|
|
// always fall back to fopen
|
|
|
|
$target = $this->fopen($path, 'w');
|
2020-03-27 17:47:20 +01:00
|
|
|
[$count, $result] = \OC_Helper::streamCopy($stream, $target);
|
2021-09-21 17:40:19 +02:00
|
|
|
fclose($stream);
|
2018-10-31 16:16:37 +01:00
|
|
|
fclose($target);
|
2022-04-13 16:05:45 +02:00
|
|
|
|
|
|
|
// object store, stores the size after write and doesn't update this during scan
|
|
|
|
// manually store the unencrypted size
|
2023-06-14 14:38:33 +02:00
|
|
|
if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class) && $this->shouldEncrypt($path)) {
|
2022-04-13 16:05:45 +02:00
|
|
|
$this->getCache()->put($path, ['unencrypted_size' => $count]);
|
|
|
|
}
|
|
|
|
|
2018-10-31 16:16:37 +01:00
|
|
|
return $count;
|
|
|
|
}
|
2022-11-30 15:11:27 +01:00
|
|
|
|
|
|
|
public function clearIsEncryptedCache(): void {
|
|
|
|
$this->encryptedPaths->clear();
|
|
|
|
}
|
2023-01-10 13:48:31 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Allow temporarily disabling the wrapper
|
|
|
|
*/
|
|
|
|
public function setEnabled(bool $enabled): void {
|
|
|
|
$this->enabled = $enabled;
|
|
|
|
}
|
2015-01-14 20:39:23 +01:00
|
|
|
}
|