Passed
Push — master ( 7d3375...7251ed )
by Robin
15:19 queued 13s
created

Encryption::copyBetweenStorage()   C

Complexity

Conditions 17
Paths 107

Size

Total Lines 83
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 17
eloc 49
c 1
b 1
f 0
nc 107
nop 5
dl 0
loc 83
rs 5.1583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author J0WI <[email protected]>
10
 * @author jknockaert <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Piotr M <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author Tigran Mkrtchyan <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
37
namespace OC\Files\Storage\Wrapper;
38
39
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
40
use OC\Encryption\Update;
41
use OC\Encryption\Util;
42
use OC\Files\Cache\CacheEntry;
43
use OC\Files\Filesystem;
44
use OC\Files\Mount\Manager;
45
use OC\Files\ObjectStore\ObjectStoreStorage;
46
use OC\Files\Storage\LocalTempFileTrait;
47
use OC\Memcache\ArrayCache;
48
use OCP\Cache\CappedMemoryCache;
49
use OCP\Encryption\Exceptions\GenericEncryptionException;
50
use OCP\Encryption\IFile;
51
use OCP\Encryption\IManager;
52
use OCP\Encryption\Keys\IStorage;
53
use OCP\Files\Cache\ICacheEntry;
54
use OCP\Files\Mount\IMountPoint;
55
use OCP\Files\Storage;
56
use Psr\Log\LoggerInterface;
57
58
class Encryption extends Wrapper {
59
	use LocalTempFileTrait;
60
61
	/** @var string */
62
	private $mountPoint;
63
64
	/** @var \OC\Encryption\Util */
65
	private $util;
66
67
	/** @var \OCP\Encryption\IManager */
68
	private $encryptionManager;
69
70
	private LoggerInterface $logger;
71
72
	/** @var string */
73
	private $uid;
74
75
	/** @var array */
76
	protected $unencryptedSize;
77
78
	/** @var \OCP\Encryption\IFile */
79
	private $fileHelper;
80
81
	/** @var IMountPoint */
82
	private $mount;
83
84
	/** @var IStorage */
85
	private $keyStorage;
86
87
	/** @var Update */
88
	private $update;
89
90
	/** @var Manager */
91
	private $mountManager;
92
93
	/** @var array remember for which path we execute the repair step to avoid recursions */
94
	private $fixUnencryptedSizeOf = [];
95
96
	/** @var  ArrayCache */
97
	private $arrayCache;
98
99
	/** @var CappedMemoryCache<bool> */
100
	private CappedMemoryCache $encryptedPaths;
101
102
	/**
103
	 * @param array $parameters
104
	 */
105
	public function __construct(
106
		$parameters,
107
		IManager $encryptionManager = null,
108
		Util $util = null,
109
		LoggerInterface $logger = null,
110
		IFile $fileHelper = null,
111
		$uid = null,
112
		IStorage $keyStorage = null,
113
		Update $update = null,
114
		Manager $mountManager = null,
115
		ArrayCache $arrayCache = null
116
	) {
117
		$this->mountPoint = $parameters['mountPoint'];
118
		$this->mount = $parameters['mount'];
119
		$this->encryptionManager = $encryptionManager;
120
		$this->util = $util;
121
		$this->logger = $logger;
122
		$this->uid = $uid;
123
		$this->fileHelper = $fileHelper;
124
		$this->keyStorage = $keyStorage;
125
		$this->unencryptedSize = [];
126
		$this->update = $update;
127
		$this->mountManager = $mountManager;
128
		$this->arrayCache = $arrayCache;
129
		$this->encryptedPaths = new CappedMemoryCache();
130
		parent::__construct($parameters);
131
	}
132
133
	/**
134
	 * see https://www.php.net/manual/en/function.filesize.php
135
	 * The result for filesize when called on a folder is required to be 0
136
	 *
137
	 * @param string $path
138
	 * @return int
139
	 */
140
	public function filesize($path) {
141
		$fullPath = $this->getFullPath($path);
142
143
		/** @var CacheEntry $info */
144
		$info = $this->getCache()->get($path);
145
		if (isset($this->unencryptedSize[$fullPath])) {
146
			$size = $this->unencryptedSize[$fullPath];
147
			// update file cache
148
			if ($info instanceof ICacheEntry) {
0 ignored issues
show
introduced by
$info is always a sub-type of OCP\Files\Cache\ICacheEntry.
Loading history...
149
				$info['encrypted'] = $info['encryptedVersion'];
150
			} else {
151
				if (!is_array($info)) {
152
					$info = [];
153
				}
154
				$info['encrypted'] = true;
155
				$info = new CacheEntry($info);
156
			}
157
158
			if ($size !== $info->getUnencryptedSize()) {
159
				$this->getCache()->update($info->getId(), [
160
					'unencrypted_size' => $size
161
				]);
162
			}
163
164
			return $size;
165
		}
166
167
		if (isset($info['fileid']) && $info['encrypted']) {
168
			return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
169
		}
170
171
		return $this->storage->filesize($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->storage->filesize($path) also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
172
	}
173
174
	/**
175
	 * @param string $path
176
	 * @param array $data
177
	 * @return array
178
	 */
179
	private function modifyMetaData(string $path, array $data): array {
180
		$fullPath = $this->getFullPath($path);
181
		$info = $this->getCache()->get($path);
182
183
		if (isset($this->unencryptedSize[$fullPath])) {
184
			$data['encrypted'] = true;
185
			$data['size'] = $this->unencryptedSize[$fullPath];
186
		} else {
187
			if (isset($info['fileid']) && $info['encrypted']) {
188
				$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
189
				$data['encrypted'] = true;
190
			}
191
		}
192
193
		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
194
			$data['encryptedVersion'] = $info['encryptedVersion'];
195
		}
196
197
		return $data;
198
	}
199
200
	public function getMetaData($path) {
201
		$data = $this->storage->getMetaData($path);
202
		if (is_null($data)) {
203
			return null;
204
		}
205
		return $this->modifyMetaData($path, $data);
206
	}
207
208
	public function getDirectoryContent($directory): \Traversable {
209
		$parent = rtrim($directory, '/');
210
		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
211
			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
212
		}
213
	}
214
215
	/**
216
	 * see https://www.php.net/manual/en/function.file_get_contents.php
217
	 *
218
	 * @param string $path
219
	 * @return string
220
	 */
221
	public function file_get_contents($path) {
222
		$encryptionModule = $this->getEncryptionModule($path);
223
224
		if ($encryptionModule) {
225
			$handle = $this->fopen($path, "r");
226
			if (!$handle) {
227
				return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
228
			}
229
			$data = stream_get_contents($handle);
230
			fclose($handle);
231
			return $data;
232
		}
233
		return $this->storage->file_get_contents($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->storage->file_get_contents($path) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
234
	}
235
236
	/**
237
	 * see https://www.php.net/manual/en/function.file_put_contents.php
238
	 *
239
	 * @param string $path
240
	 * @param mixed $data
241
	 * @return int|false
242
	 */
243
	public function file_put_contents($path, $data) {
244
		// file put content will always be translated to a stream write
245
		$handle = $this->fopen($path, 'w');
246
		if (is_resource($handle)) {
247
			$written = fwrite($handle, $data);
248
			fclose($handle);
249
			return $written;
250
		}
251
252
		return false;
253
	}
254
255
	/**
256
	 * see https://www.php.net/manual/en/function.unlink.php
257
	 *
258
	 * @param string $path
259
	 * @return bool
260
	 */
261
	public function unlink($path) {
262
		$fullPath = $this->getFullPath($path);
263
		if ($this->util->isExcluded($fullPath)) {
264
			return $this->storage->unlink($path);
265
		}
266
267
		$encryptionModule = $this->getEncryptionModule($path);
268
		if ($encryptionModule) {
269
			$this->keyStorage->deleteAllFileKeys($fullPath);
270
		}
271
272
		return $this->storage->unlink($path);
273
	}
274
275
	/**
276
	 * see https://www.php.net/manual/en/function.rename.php
277
	 *
278
	 * @param string $source
279
	 * @param string $target
280
	 * @return bool
281
	 */
282
	public function rename($source, $target) {
283
		$result = $this->storage->rename($source, $target);
284
285
		if ($result &&
286
			// versions always use the keys from the original file, so we can skip
287
			// this step for versions
288
			$this->isVersion($target) === false &&
289
			$this->encryptionManager->isEnabled()) {
290
			$sourcePath = $this->getFullPath($source);
291
			if (!$this->util->isExcluded($sourcePath)) {
292
				$targetPath = $this->getFullPath($target);
293
				if (isset($this->unencryptedSize[$sourcePath])) {
294
					$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
295
				}
296
				$this->keyStorage->renameKeys($sourcePath, $targetPath);
297
				$module = $this->getEncryptionModule($target);
298
				if ($module) {
299
					$module->update($targetPath, $this->uid, []);
300
				}
301
			}
302
		}
303
304
		return $result;
305
	}
306
307
	/**
308
	 * see https://www.php.net/manual/en/function.rmdir.php
309
	 *
310
	 * @param string $path
311
	 * @return bool
312
	 */
313
	public function rmdir($path) {
314
		$result = $this->storage->rmdir($path);
315
		$fullPath = $this->getFullPath($path);
316
		if ($result &&
317
			$this->util->isExcluded($fullPath) === false &&
318
			$this->encryptionManager->isEnabled()
319
		) {
320
			$this->keyStorage->deleteAllFileKeys($fullPath);
321
		}
322
323
		return $result;
324
	}
325
326
	/**
327
	 * check if a file can be read
328
	 *
329
	 * @param string $path
330
	 * @return bool
331
	 */
332
	public function isReadable($path) {
333
		$isReadable = true;
334
335
		$metaData = $this->getMetaData($path);
336
		if (
337
			!$this->is_dir($path) &&
338
			isset($metaData['encrypted']) &&
339
			$metaData['encrypted'] === true
340
		) {
341
			$fullPath = $this->getFullPath($path);
342
			$module = $this->getEncryptionModule($path);
343
			$isReadable = $module->isReadable($fullPath, $this->uid);
344
		}
345
346
		return $this->storage->isReadable($path) && $isReadable;
347
	}
348
349
	/**
350
	 * see https://www.php.net/manual/en/function.copy.php
351
	 *
352
	 * @param string $source
353
	 * @param string $target
354
	 */
355
	public function copy($source, $target): bool {
356
		$sourcePath = $this->getFullPath($source);
357
358
		if ($this->util->isExcluded($sourcePath)) {
359
			return $this->storage->copy($source, $target);
360
		}
361
362
		// need to stream copy file by file in case we copy between a encrypted
363
		// and a unencrypted storage
364
		$this->unlink($target);
365
		return $this->copyFromStorage($this, $source, $target);
366
	}
367
368
	/**
369
	 * see https://www.php.net/manual/en/function.fopen.php
370
	 *
371
	 * @param string $path
372
	 * @param string $mode
373
	 * @return resource|bool
374
	 * @throws GenericEncryptionException
375
	 * @throws ModuleDoesNotExistsException
376
	 */
377
	public function fopen($path, $mode) {
378
		// check if the file is stored in the array cache, this means that we
379
		// copy a file over to the versions folder, in this case we don't want to
380
		// decrypt it
381
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
382
			$this->arrayCache->remove('encryption_copy_version_' . $path);
383
			return $this->storage->fopen($path, $mode);
384
		}
385
386
		$encryptionEnabled = $this->encryptionManager->isEnabled();
387
		$shouldEncrypt = false;
388
		$encryptionModule = null;
389
		$header = $this->getHeader($path);
390
		$signed = isset($header['signed']) && $header['signed'] === 'true';
391
		$fullPath = $this->getFullPath($path);
392
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
393
394
		if ($this->util->isExcluded($fullPath) === false) {
395
			$size = $unencryptedSize = 0;
396
			$realFile = $this->util->stripPartialFileExtension($path);
397
			$targetExists = $this->is_file($realFile) || $this->file_exists($path);
398
			$targetIsEncrypted = false;
399
			if ($targetExists) {
400
				// in case the file exists we require the explicit module as
401
				// specified in the file header - otherwise we need to fail hard to
402
				// prevent data loss on client side
403
				if (!empty($encryptionModuleId)) {
404
					$targetIsEncrypted = true;
405
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
406
				}
407
408
				if ($this->file_exists($path)) {
409
					$size = $this->storage->filesize($path);
410
					$unencryptedSize = $this->filesize($path);
411
				} else {
412
					$size = $unencryptedSize = 0;
413
				}
414
			}
415
416
			try {
417
				if (
418
					$mode === 'w'
419
					|| $mode === 'w+'
420
					|| $mode === 'wb'
421
					|| $mode === 'wb+'
422
				) {
423
					// if we update a encrypted file with a un-encrypted one we change the db flag
424
					if ($targetIsEncrypted && $encryptionEnabled === false) {
425
						$cache = $this->storage->getCache();
426
						if ($cache) {
0 ignored issues
show
introduced by
$cache is of type OC\Files\Cache\Cache, thus it always evaluated to true.
Loading history...
427
							$entry = $cache->get($path);
428
							$cache->update($entry->getId(), ['encrypted' => 0]);
429
						}
430
					}
431
					if ($encryptionEnabled) {
432
						// if $encryptionModuleId is empty, the default module will be used
433
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
434
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
435
						$signed = true;
436
					}
437
				} else {
438
					$info = $this->getCache()->get($path);
439
					// only get encryption module if we found one in the header
440
					// or if file should be encrypted according to the file cache
441
					if (!empty($encryptionModuleId)) {
442
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
443
						$shouldEncrypt = true;
444
					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
445
						// we come from a old installation. No header and/or no module defined
446
						// but the file is encrypted. In this case we need to use the
447
						// OC_DEFAULT_MODULE to read the file
448
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
449
						$shouldEncrypt = true;
450
						$targetIsEncrypted = true;
451
					}
452
				}
453
			} catch (ModuleDoesNotExistsException $e) {
454
				$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
455
					'exception' => $e,
456
					'app' => 'core',
457
				]);
458
			}
459
460
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
461
			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
462
				if (!$targetExists || !$targetIsEncrypted) {
463
					$shouldEncrypt = false;
464
				}
465
			}
466
467
			if ($shouldEncrypt === true && $encryptionModule !== null) {
468
				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type OCP\Cache\T expected by parameter $value of OCP\Cache\CappedMemoryCache::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

468
				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), /** @scrutinizer ignore-type */ true);
Loading history...
469
				$headerSize = $this->getHeaderSize($path);
470
				$source = $this->storage->fopen($path, $mode);
471
				if (!is_resource($source)) {
472
					return false;
473
				}
474
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
475
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
476
					$size, $unencryptedSize, $headerSize, $signed);
477
478
				return $handle;
479
			}
480
		}
481
482
		return $this->storage->fopen($path, $mode);
483
	}
484
485
486
	/**
487
	 * perform some plausibility checks if the the unencrypted size is correct.
488
	 * If not, we calculate the correct unencrypted size and return it
489
	 *
490
	 * @param string $path internal path relative to the storage root
491
	 * @param int $unencryptedSize size of the unencrypted file
492
	 *
493
	 * @return int unencrypted size
494
	 */
495
	protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
496
		$size = $this->storage->filesize($path);
497
		$result = $unencryptedSize;
498
499
		if ($unencryptedSize < 0 ||
500
			($size > 0 && $unencryptedSize === $size)
501
		) {
502
			// check if we already calculate the unencrypted size for the
503
			// given path to avoid recursions
504
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
505
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
506
				try {
507
					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
508
				} catch (\Exception $e) {
509
					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
510
				}
511
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
512
			}
513
		}
514
515
		return $result;
516
	}
517
518
	/**
519
	 * calculate the unencrypted size
520
	 *
521
	 * @param string $path internal path relative to the storage root
522
	 * @param int $size size of the physical file
523
	 * @param int $unencryptedSize size of the unencrypted file
524
	 *
525
	 * @return int calculated unencrypted size
526
	 */
527
	protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
528
		$headerSize = $this->getHeaderSize($path);
529
		$header = $this->getHeader($path);
530
		$encryptionModule = $this->getEncryptionModule($path);
531
532
		$stream = $this->storage->fopen($path, 'r');
533
534
		// if we couldn't open the file we return the old unencrypted size
535
		if (!is_resource($stream)) {
536
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
537
			return $unencryptedSize;
538
		}
539
540
		$newUnencryptedSize = 0;
541
		$size -= $headerSize;
542
		$blockSize = $this->util->getBlockSize();
543
544
		// if a header exists we skip it
545
		if ($headerSize > 0) {
546
			$this->fread_block($stream, $headerSize);
0 ignored issues
show
Bug introduced by
$stream of type resource is incompatible with the type OC\Files\Storage\Wrapper\the expected by parameter $handle of OC\Files\Storage\Wrapper\Encryption::fread_block(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

546
			$this->fread_block(/** @scrutinizer ignore-type */ $stream, $headerSize);
Loading history...
547
		}
548
549
		// fast path, else the calculation for $lastChunkNr is bogus
550
		if ($size === 0) {
551
			return 0;
552
		}
553
554
		$signed = isset($header['signed']) && $header['signed'] === 'true';
555
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
556
557
		// calculate last chunk nr
558
		// next highest is end of chunks, one subtracted is last one
559
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
560
561
		$lastChunkNr = ceil($size / $blockSize) - 1;
562
		// calculate last chunk position
563
		$lastChunkPos = ($lastChunkNr * $blockSize);
564
		// try to fseek to the last chunk, if it fails we have to read the whole file
565
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
0 ignored issues
show
Bug introduced by
$lastChunkPos of type double is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

565
		if (@fseek($stream, /** @scrutinizer ignore-type */ $lastChunkPos, SEEK_CUR) === 0) {
Loading history...
566
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
567
		}
568
569
		$lastChunkContentEncrypted = '';
570
		$count = $blockSize;
571
572
		while ($count > 0) {
573
			$data = $this->fread_block($stream, $blockSize);
574
			$count = strlen($data);
575
			$lastChunkContentEncrypted .= $data;
576
			if (strlen($lastChunkContentEncrypted) > $blockSize) {
577
				$newUnencryptedSize += $unencryptedBlockSize;
578
				$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
579
			}
580
		}
581
582
		fclose($stream);
583
584
		// we have to decrypt the last chunk to get it actual size
585
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
586
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
587
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
588
589
		// calc the real file size with the size of the last chunk
590
		$newUnencryptedSize += strlen($decryptedLastChunk);
591
592
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
593
594
		// write to cache if applicable
595
		$cache = $this->storage->getCache();
596
		if ($cache) {
0 ignored issues
show
introduced by
$cache is of type OC\Files\Cache\Cache, thus it always evaluated to true.
Loading history...
597
			$entry = $cache->get($path);
598
			$cache->update($entry['fileid'], [
599
				'unencrypted_size' => $newUnencryptedSize
600
			]);
601
		}
602
603
		return $newUnencryptedSize;
604
	}
605
606
	/**
607
	 * fread_block
608
	 *
609
	 * This function is a wrapper around the fread function.  It is based on the
610
	 * stream_read_block function from lib/private/Files/Streams/Encryption.php
611
	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
612
	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
613
	 * remote storage over the internet and it does not care about the given $blockSize.
614
	 *
615
	 * @param handle the stream to read from
0 ignored issues
show
Bug introduced by
The type OC\Files\Storage\Wrapper\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
616
	 * @param int $blockSize Length of requested data block in bytes
617
	 * @return string Data fetched from stream.
618
	 */
619
	private function fread_block($handle, int $blockSize): string {
620
		$remaining = $blockSize;
621
		$data = '';
622
623
		do {
624
			$chunk = fread($handle, $remaining);
0 ignored issues
show
Bug introduced by
$handle of type OC\Files\Storage\Wrapper\the is incompatible with the type resource expected by parameter $stream of fread(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

624
			$chunk = fread(/** @scrutinizer ignore-type */ $handle, $remaining);
Loading history...
625
			$chunk_len = strlen($chunk);
626
			$data .= $chunk;
627
			$remaining -= $chunk_len;
628
		} while (($remaining > 0) && ($chunk_len > 0));
629
630
		return $data;
631
	}
632
633
	/**
634
	 * @param Storage\IStorage $sourceStorage
635
	 * @param string $sourceInternalPath
636
	 * @param string $targetInternalPath
637
	 * @param bool $preserveMtime
638
	 * @return bool
639
	 */
640
	public function moveFromStorage(
641
		Storage\IStorage $sourceStorage,
642
		$sourceInternalPath,
643
		$targetInternalPath,
644
		$preserveMtime = true
645
	) {
646
		if ($sourceStorage === $this) {
0 ignored issues
show
introduced by
The condition $sourceStorage === $this is always false.
Loading history...
647
			return $this->rename($sourceInternalPath, $targetInternalPath);
648
		}
649
650
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
651
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
652
		// - copy the file cache update from  $this->copyBetweenStorage to this method
653
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
654
		// - remove $this->copyBetweenStorage
655
656
		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
657
			return false;
658
		}
659
660
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
661
		if ($result) {
662
			if ($sourceStorage->is_dir($sourceInternalPath)) {
663
				$result &= $sourceStorage->rmdir($sourceInternalPath);
664
			} else {
665
				$result &= $sourceStorage->unlink($sourceInternalPath);
666
			}
667
		}
668
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
669
	}
670
671
672
	/**
673
	 * @param Storage\IStorage $sourceStorage
674
	 * @param string $sourceInternalPath
675
	 * @param string $targetInternalPath
676
	 * @param bool $preserveMtime
677
	 * @param bool $isRename
678
	 * @return bool
679
	 */
680
	public function copyFromStorage(
681
		Storage\IStorage $sourceStorage,
682
		$sourceInternalPath,
683
		$targetInternalPath,
684
		$preserveMtime = false,
685
		$isRename = false
686
	) {
687
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
688
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
689
		// - copy the file cache update from  $this->copyBetweenStorage to this method
690
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
691
		// - remove $this->copyBetweenStorage
692
693
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
694
	}
695
696
	/**
697
	 * Update the encrypted cache version in the database
698
	 *
699
	 * @param Storage\IStorage $sourceStorage
700
	 * @param string $sourceInternalPath
701
	 * @param string $targetInternalPath
702
	 * @param bool $isRename
703
	 * @param bool $keepEncryptionVersion
704
	 */
705
	private function updateEncryptedVersion(
706
		Storage\IStorage $sourceStorage,
707
		$sourceInternalPath,
708
		$targetInternalPath,
709
		$isRename,
710
		$keepEncryptionVersion
711
	) {
712
		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
713
		$cacheInformation = [
714
			'encrypted' => $isEncrypted,
715
		];
716
		if ($isEncrypted) {
717
			$sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
718
			$targetCacheEntry = $this->getCache()->get($targetInternalPath);
719
720
			// Rename of the cache already happened, so we do the cleanup on the target
721
			if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
722
				$encryptedVersion = $targetCacheEntry['encryptedVersion'];
723
				$isRename = false;
724
			} else {
725
				$encryptedVersion = $sourceCacheEntry['encryptedVersion'];
726
			}
727
728
			// In case of a move operation from an unencrypted to an encrypted
729
			// storage the old encrypted version would stay with "0" while the
730
			// correct value would be "1". Thus we manually set the value to "1"
731
			// for those cases.
732
			// See also https://github.com/owncloud/core/issues/23078
733
			if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
734
				$encryptedVersion = 1;
735
			}
736
737
			$cacheInformation['encryptedVersion'] = $encryptedVersion;
738
		}
739
740
		// in case of a rename we need to manipulate the source cache because
741
		// this information will be kept for the new target
742
		if ($isRename) {
743
			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
744
		} else {
745
			$this->getCache()->put($targetInternalPath, $cacheInformation);
746
		}
747
	}
748
749
	/**
750
	 * copy file between two storages
751
	 *
752
	 * @param Storage\IStorage $sourceStorage
753
	 * @param string $sourceInternalPath
754
	 * @param string $targetInternalPath
755
	 * @param bool $preserveMtime
756
	 * @param bool $isRename
757
	 * @return bool
758
	 * @throws \Exception
759
	 */
760
	private function copyBetweenStorage(
761
		Storage\IStorage $sourceStorage,
762
		$sourceInternalPath,
763
		$targetInternalPath,
764
		$preserveMtime,
765
		$isRename
766
	) {
767
		// for versions we have nothing to do, because versions should always use the
768
		// key from the original file. Just create a 1:1 copy and done
769
		if ($this->isVersion($targetInternalPath) ||
770
			$this->isVersion($sourceInternalPath)) {
771
			// remember that we try to create a version so that we can detect it during
772
			// fopen($sourceInternalPath) and by-pass the encryption in order to
773
			// create a 1:1 copy of the file
774
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
775
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
776
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
777
			if ($result) {
778
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
779
				// make sure that we update the unencrypted size for the version
780
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
781
					$this->updateUnencryptedSize(
782
						$this->getFullPath($targetInternalPath),
783
						$info->getUnencryptedSize()
784
					);
785
				}
786
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
787
			}
788
			return $result;
789
		}
790
791
		// first copy the keys that we reuse the existing file key on the target location
792
		// and don't create a new one which would break versions for example.
793
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
794
		if (count($mount) === 1) {
795
			$mountPoint = $mount[0]->getMountPoint();
796
			$source = $mountPoint . '/' . $sourceInternalPath;
797
			$target = $this->getFullPath($targetInternalPath);
798
			$this->copyKeys($source, $target);
799
		} else {
800
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
801
		}
802
803
		if ($sourceStorage->is_dir($sourceInternalPath)) {
804
			$dh = $sourceStorage->opendir($sourceInternalPath);
805
			if (!$this->is_dir($targetInternalPath)) {
806
				$result = $this->mkdir($targetInternalPath);
807
			} else {
808
				$result = true;
809
			}
810
			if (is_resource($dh)) {
811
				while ($result and ($file = readdir($dh)) !== false) {
812
					if (!Filesystem::isIgnoredDir($file)) {
813
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
814
					}
815
				}
816
			}
817
		} else {
818
			try {
819
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
820
				$target = $this->fopen($targetInternalPath, 'w');
821
				[, $result] = \OC_Helper::streamCopy($source, $target);
822
			} finally {
823
				if (is_resource($source)) {
824
					fclose($source);
825
				}
826
				if (is_resource($target)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $target does not seem to be defined for all execution paths leading up to this point.
Loading history...
827
					fclose($target);
828
				}
829
			}
830
			if ($result) {
831
				if ($preserveMtime) {
832
					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
833
				}
834
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
835
			} else {
836
				// delete partially written target file
837
				$this->unlink($targetInternalPath);
838
				// delete cache entry that was created by fopen
839
				$this->getCache()->remove($targetInternalPath);
840
			}
841
		}
842
		return (bool)$result;
843
	}
844
845
	public function getLocalFile($path) {
846
		if ($this->encryptionManager->isEnabled()) {
847
			$cachedFile = $this->getCachedFile($path);
848
			if (is_string($cachedFile)) {
0 ignored issues
show
introduced by
The condition is_string($cachedFile) is always true.
Loading history...
849
				return $cachedFile;
850
			}
851
		}
852
		return $this->storage->getLocalFile($path);
853
	}
854
855
	public function isLocal() {
856
		if ($this->encryptionManager->isEnabled()) {
857
			return false;
858
		}
859
		return $this->storage->isLocal();
860
	}
861
862
	public function stat($path) {
863
		$stat = $this->storage->stat($path);
864
		if (!$stat) {
865
			return false;
866
		}
867
		$fileSize = $this->filesize($path);
868
		$stat['size'] = $fileSize;
869
		$stat[7] = $fileSize;
870
		$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
871
		return $stat;
872
	}
873
874
	public function hash($type, $path, $raw = false) {
875
		$fh = $this->fopen($path, 'rb');
876
		$ctx = hash_init($type);
877
		hash_update_stream($ctx, $fh);
878
		fclose($fh);
879
		return hash_final($ctx, $raw);
880
	}
881
882
	/**
883
	 * return full path, including mount point
884
	 *
885
	 * @param string $path relative to mount point
886
	 * @return string full path including mount point
887
	 */
888
	protected function getFullPath($path) {
889
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
890
	}
891
892
	/**
893
	 * read first block of encrypted file, typically this will contain the
894
	 * encryption header
895
	 *
896
	 * @param string $path
897
	 * @return string
898
	 */
899
	protected function readFirstBlock($path) {
900
		$firstBlock = '';
901
		if ($this->storage->is_file($path)) {
902
			$handle = $this->storage->fopen($path, 'r');
903
			$firstBlock = fread($handle, $this->util->getHeaderSize());
904
			fclose($handle);
905
		}
906
		return $firstBlock;
907
	}
908
909
	/**
910
	 * return header size of given file
911
	 *
912
	 * @param string $path
913
	 * @return int
914
	 */
915
	protected function getHeaderSize($path) {
916
		$headerSize = 0;
917
		$realFile = $this->util->stripPartialFileExtension($path);
918
		if ($this->storage->is_file($realFile)) {
919
			$path = $realFile;
920
		}
921
		$firstBlock = $this->readFirstBlock($path);
922
923
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
924
			$headerSize = $this->util->getHeaderSize();
925
		}
926
927
		return $headerSize;
928
	}
929
930
	/**
931
	 * parse raw header to array
932
	 *
933
	 * @param string $rawHeader
934
	 * @return array
935
	 */
936
	protected function parseRawHeader($rawHeader) {
937
		$result = [];
938
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
939
			$header = $rawHeader;
940
			$endAt = strpos($header, Util::HEADER_END);
941
			if ($endAt !== false) {
942
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
943
944
				// +1 to not start with an ':' which would result in empty element at the beginning
945
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
946
947
				$element = array_shift($exploded);
948
				while ($element !== Util::HEADER_END) {
949
					$result[$element] = array_shift($exploded);
950
					$element = array_shift($exploded);
951
				}
952
			}
953
		}
954
955
		return $result;
956
	}
957
958
	/**
959
	 * read header from file
960
	 *
961
	 * @param string $path
962
	 * @return array
963
	 */
964
	protected function getHeader($path) {
965
		$realFile = $this->util->stripPartialFileExtension($path);
966
		$exists = $this->storage->is_file($realFile);
967
		if ($exists) {
968
			$path = $realFile;
969
		}
970
971
		$result = [];
972
973
		$isEncrypted = $this->encryptedPaths->get($realFile);
974
		if (is_null($isEncrypted)) {
975
			$info = $this->getCache()->get($path);
976
			$isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
977
		}
978
979
		if ($isEncrypted) {
980
			$firstBlock = $this->readFirstBlock($path);
981
			$result = $this->parseRawHeader($firstBlock);
982
983
			// if the header doesn't contain a encryption module we check if it is a
984
			// legacy file. If true, we add the default encryption module
985
			if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
986
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
987
			}
988
		}
989
990
		return $result;
991
	}
992
993
	/**
994
	 * read encryption module needed to read/write the file located at $path
995
	 *
996
	 * @param string $path
997
	 * @return null|\OCP\Encryption\IEncryptionModule
998
	 * @throws ModuleDoesNotExistsException
999
	 * @throws \Exception
1000
	 */
1001
	protected function getEncryptionModule($path) {
1002
		$encryptionModule = null;
1003
		$header = $this->getHeader($path);
1004
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
1005
		if (!empty($encryptionModuleId)) {
1006
			try {
1007
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1008
			} catch (ModuleDoesNotExistsException $e) {
1009
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1010
				throw $e;
1011
			}
1012
		}
1013
1014
		return $encryptionModule;
1015
	}
1016
1017
	/**
1018
	 * @param string $path
1019
	 * @param int $unencryptedSize
1020
	 */
1021
	public function updateUnencryptedSize($path, $unencryptedSize) {
1022
		$this->unencryptedSize[$path] = $unencryptedSize;
1023
	}
1024
1025
	/**
1026
	 * copy keys to new location
1027
	 *
1028
	 * @param string $source path relative to data/
1029
	 * @param string $target path relative to data/
1030
	 * @return bool
1031
	 */
1032
	protected function copyKeys($source, $target) {
1033
		if (!$this->util->isExcluded($source)) {
1034
			return $this->keyStorage->copyKeys($source, $target);
1035
		}
1036
1037
		return false;
1038
	}
1039
1040
	/**
1041
	 * check if path points to a files version
1042
	 *
1043
	 * @param $path
1044
	 * @return bool
1045
	 */
1046
	protected function isVersion($path) {
1047
		$normalized = Filesystem::normalizePath($path);
1048
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1049
	}
1050
1051
	/**
1052
	 * check if the given storage should be encrypted or not
1053
	 *
1054
	 * @param $path
1055
	 * @return bool
1056
	 */
1057
	protected function shouldEncrypt($path) {
1058
		$fullPath = $this->getFullPath($path);
1059
		$mountPointConfig = $this->mount->getOption('encrypt', true);
1060
		if ($mountPointConfig === false) {
1061
			return false;
1062
		}
1063
1064
		try {
1065
			$encryptionModule = $this->getEncryptionModule($fullPath);
1066
		} catch (ModuleDoesNotExistsException $e) {
1067
			return false;
1068
		}
1069
1070
		if ($encryptionModule === null) {
1071
			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1072
		}
1073
1074
		return $encryptionModule->shouldEncrypt($fullPath);
1075
	}
1076
1077
	public function writeStream(string $path, $stream, int $size = null): int {
1078
		// always fall back to fopen
1079
		$target = $this->fopen($path, 'w');
1080
		[$count, $result] = \OC_Helper::streamCopy($stream, $target);
1081
		fclose($stream);
1082
		fclose($target);
1083
1084
		// object store, stores the size after write and doesn't update this during scan
1085
		// manually store the unencrypted size
1086
		if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
1087
			$this->getCache()->put($path, ['unencrypted_size' => $count]);
1088
		}
1089
1090
		return $count;
1091
	}
1092
1093
	public function clearIsEncryptedCache(): void {
1094
		$this->encryptedPaths->clear();
1095
	}
1096
}
1097