Passed
Push — master ( 7d3375...7251ed )
by Robin
15:19 queued 13s
created
lib/private/Files/Storage/Wrapper/Encryption.php 1 patch
Indentation   +1037 added lines, -1037 removed lines patch added patch discarded remove patch
@@ -56,1041 +56,1041 @@
 block discarded – undo
56 56
 use Psr\Log\LoggerInterface;
57 57
 
58 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) {
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);
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;
228
-			}
229
-			$data = stream_get_contents($handle);
230
-			fclose($handle);
231
-			return $data;
232
-		}
233
-		return $this->storage->file_get_contents($path);
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) {
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);
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);
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) {
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) {
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
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);
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) {
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;
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)) {
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)) {
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
-	}
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) {
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);
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;
228
+            }
229
+            $data = stream_get_contents($handle);
230
+            fclose($handle);
231
+            return $data;
232
+        }
233
+        return $this->storage->file_get_contents($path);
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) {
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);
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);
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) {
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) {
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
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);
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) {
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;
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)) {
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)) {
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 1096
 }
Please login to merge, or discard this patch.