Completed
Push — master ( dccb89...31024b )
by Morris
12:39
created

Encryption::parseRawHeader()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 4
Ratio 19.05 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 3
nop 1
dl 4
loc 21
rs 9.0534
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Björn Schießle <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Robin Appelman <[email protected]>
9
 * @author Thomas Müller <[email protected]>
10
 * @author Vincent Petry <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OC\Files\Storage\Wrapper;
29
30
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
31
use OC\Encryption\Update;
32
use OC\Encryption\Util;
33
use OC\Files\Cache\CacheEntry;
34
use OC\Files\Filesystem;
35
use OC\Files\Mount\Manager;
36
use OC\Files\Storage\LocalTempFileTrait;
37
use OC\Memcache\ArrayCache;
38
use OCP\Encryption\Exceptions\GenericEncryptionException;
39
use OCP\Encryption\IFile;
40
use OCP\Encryption\IManager;
41
use OCP\Encryption\Keys\IStorage;
42
use OCP\Files\Mount\IMountPoint;
43
use OCP\Files\Storage;
44
use OCP\ILogger;
45
use OCP\Files\Cache\ICacheEntry;
46
47
class Encryption extends Wrapper {
48
49
	use LocalTempFileTrait;
50
51
	/** @var string */
52
	private $mountPoint;
53
54
	/** @var \OC\Encryption\Util */
55
	private $util;
56
57
	/** @var \OCP\Encryption\IManager */
58
	private $encryptionManager;
59
60
	/** @var \OCP\ILogger */
61
	private $logger;
62
63
	/** @var string */
64
	private $uid;
65
66
	/** @var array */
67
	protected $unencryptedSize;
68
69
	/** @var \OCP\Encryption\IFile */
70
	private $fileHelper;
71
72
	/** @var IMountPoint */
73
	private $mount;
74
75
	/** @var IStorage */
76
	private $keyStorage;
77
78
	/** @var Update */
79
	private $update;
80
81
	/** @var Manager */
82
	private $mountManager;
83
84
	/** @var array remember for which path we execute the repair step to avoid recursions */
85
	private $fixUnencryptedSizeOf = array();
86
87
	/** @var  ArrayCache */
88
	private $arrayCache;
89
90
	/**
91
	 * @param array $parameters
92
	 * @param IManager $encryptionManager
93
	 * @param Util $util
94
	 * @param ILogger $logger
95
	 * @param IFile $fileHelper
96
	 * @param string $uid
97
	 * @param IStorage $keyStorage
98
	 * @param Update $update
99
	 * @param Manager $mountManager
100
	 * @param ArrayCache $arrayCache
101
	 */
102
	public function __construct(
103
			$parameters,
104
			IManager $encryptionManager = null,
105
			Util $util = null,
106
			ILogger $logger = null,
107
			IFile $fileHelper = null,
108
			$uid = null,
109
			IStorage $keyStorage = null,
110
			Update $update = null,
111
			Manager $mountManager = null,
112
			ArrayCache $arrayCache = null
113
		) {
114
115
		$this->mountPoint = $parameters['mountPoint'];
116
		$this->mount = $parameters['mount'];
117
		$this->encryptionManager = $encryptionManager;
118
		$this->util = $util;
119
		$this->logger = $logger;
120
		$this->uid = $uid;
121
		$this->fileHelper = $fileHelper;
122
		$this->keyStorage = $keyStorage;
123
		$this->unencryptedSize = array();
124
		$this->update = $update;
125
		$this->mountManager = $mountManager;
126
		$this->arrayCache = $arrayCache;
127
		parent::__construct($parameters);
128
	}
129
130
	/**
131
	 * see http://php.net/manual/en/function.filesize.php
132
	 * The result for filesize when called on a folder is required to be 0
133
	 *
134
	 * @param string $path
135
	 * @return int
136
	 */
137
	public function filesize($path) {
138
		$fullPath = $this->getFullPath($path);
139
140
		/** @var CacheEntry $info */
141
		$info = $this->getCache()->get($path);
142
		if (isset($this->unencryptedSize[$fullPath])) {
143
			$size = $this->unencryptedSize[$fullPath];
144
			// update file cache
145
			if ($info instanceof ICacheEntry) {
146
				$info = $info->getData();
147
				$info['encrypted'] = $info['encryptedVersion'];
148
			} else {
149
				if (!is_array($info)) {
150
					$info = [];
151
				}
152
				$info['encrypted'] = true;
153
			}
154
155
			$info['size'] = $size;
156
			$this->getCache()->put($path, $info);
157
158
			return $size;
159
		}
160
161
		if (isset($info['fileid']) && $info['encrypted']) {
162
			return $this->verifyUnencryptedSize($path, $info['size']);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->verifyUnencrypted...($path, $info['size']); of type integer|double adds the type double to the return on line 162 which is incompatible with the return type declared by the interface OCP\Files\Storage::filesize of type integer|false.
Loading history...
163
		}
164
165
		return $this->storage->filesize($path);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->storage->filesize($path); of type integer|false adds false to the return on line 165 which is incompatible with the return type documented by OC\Files\Storage\Wrapper\Encryption::filesize of type integer. It seems like you forgot to handle an error condition.
Loading history...
166
	}
167
168
	/**
169
	 * @param string $path
170
	 * @return array
171
	 */
172
	public function getMetaData($path) {
173
		$data = $this->storage->getMetaData($path);
174
		if (is_null($data)) {
175
			return null;
176
		}
177
		$fullPath = $this->getFullPath($path);
178
		$info = $this->getCache()->get($path);
179
180
		if (isset($this->unencryptedSize[$fullPath])) {
181
			$data['encrypted'] = true;
182
			$data['size'] = $this->unencryptedSize[$fullPath];
183
		} else {
184
			if (isset($info['fileid']) && $info['encrypted']) {
185
				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
186
				$data['encrypted'] = true;
187
			}
188
		}
189
190
		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
191
			$data['encryptedVersion'] = $info['encryptedVersion'];
192
		}
193
194
		return $data;
195
	}
196
197
	/**
198
	 * see http://php.net/manual/en/function.file_get_contents.php
199
	 *
200
	 * @param string $path
201
	 * @return string
202
	 */
203
	public function file_get_contents($path) {
204
205
		$encryptionModule = $this->getEncryptionModule($path);
206
207
		if ($encryptionModule) {
208
			$handle = $this->fopen($path, "r");
209
			if (!$handle) {
210
				return false;
211
			}
212
			$data = stream_get_contents($handle);
213
			fclose($handle);
214
			return $data;
215
		}
216
		return $this->storage->file_get_contents($path);
217
	}
218
219
	/**
220
	 * see http://php.net/manual/en/function.file_put_contents.php
221
	 *
222
	 * @param string $path
223
	 * @param string $data
224
	 * @return bool
225
	 */
226
	public function file_put_contents($path, $data) {
227
		// file put content will always be translated to a stream write
228
		$handle = $this->fopen($path, 'w');
229
		if (is_resource($handle)) {
230
			$written = fwrite($handle, $data);
231
			fclose($handle);
232
			return $written;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $written; (integer) is incompatible with the return type declared by the interface OCP\Files\Storage::file_put_contents of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
233
		}
234
235
		return false;
236
	}
237
238
	/**
239
	 * see http://php.net/manual/en/function.unlink.php
240
	 *
241
	 * @param string $path
242
	 * @return bool
243
	 */
244
	public function unlink($path) {
245
		$fullPath = $this->getFullPath($path);
246
		if ($this->util->isExcluded($fullPath)) {
247
			return $this->storage->unlink($path);
248
		}
249
250
		$encryptionModule = $this->getEncryptionModule($path);
251
		if ($encryptionModule) {
252
			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
253
		}
254
255
		return $this->storage->unlink($path);
256
	}
257
258
	/**
259
	 * see http://php.net/manual/en/function.rename.php
260
	 *
261
	 * @param string $path1
262
	 * @param string $path2
263
	 * @return bool
264
	 */
265
	public function rename($path1, $path2) {
266
267
		$result = $this->storage->rename($path1, $path2);
268
269
		if ($result &&
270
			// versions always use the keys from the original file, so we can skip
271
			// this step for versions
272
			$this->isVersion($path2) === false &&
273
			$this->encryptionManager->isEnabled()) {
274
			$source = $this->getFullPath($path1);
275
			if (!$this->util->isExcluded($source)) {
276
				$target = $this->getFullPath($path2);
277
				if (isset($this->unencryptedSize[$source])) {
278
					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
279
				}
280
				$this->keyStorage->renameKeys($source, $target);
281
				$module = $this->getEncryptionModule($path2);
282
				if ($module) {
283
					$module->update($target, $this->uid, []);
284
				}
285
			}
286
		}
287
288
		return $result;
289
	}
290
291
	/**
292
	 * see http://php.net/manual/en/function.rmdir.php
293
	 *
294
	 * @param string $path
295
	 * @return bool
296
	 */
297
	public function rmdir($path) {
298
		$result = $this->storage->rmdir($path);
299
		$fullPath = $this->getFullPath($path);
300
		if ($result &&
301
			$this->util->isExcluded($fullPath) === false &&
302
			$this->encryptionManager->isEnabled()
303
		) {
304
			$this->keyStorage->deleteAllFileKeys($fullPath);
305
		}
306
307
		return $result;
308
	}
309
310
	/**
311
	 * check if a file can be read
312
	 *
313
	 * @param string $path
314
	 * @return bool
315
	 */
316
	public function isReadable($path) {
317
318
		$isReadable = true;
319
320
		$metaData = $this->getMetaData($path);
321
		if (
322
			!$this->is_dir($path) &&
323
			isset($metaData['encrypted']) &&
324
			$metaData['encrypted'] === true
325
		) {
326
			$fullPath = $this->getFullPath($path);
327
			$module = $this->getEncryptionModule($path);
328
			$isReadable = $module->isReadable($fullPath, $this->uid);
329
		}
330
331
		return $this->storage->isReadable($path) && $isReadable;
332
	}
333
334
	/**
335
	 * see http://php.net/manual/en/function.copy.php
336
	 *
337
	 * @param string $path1
338
	 * @param string $path2
339
	 * @return bool
340
	 */
341
	public function copy($path1, $path2) {
342
343
		$source = $this->getFullPath($path1);
344
345
		if ($this->util->isExcluded($source)) {
346
			return $this->storage->copy($path1, $path2);
347
		}
348
349
		// need to stream copy file by file in case we copy between a encrypted
350
		// and a unencrypted storage
351
		$this->unlink($path2);
352
		$result = $this->copyFromStorage($this, $path1, $path2);
353
354
		return $result;
355
	}
356
357
	/**
358
	 * see http://php.net/manual/en/function.fopen.php
359
	 *
360
	 * @param string $path
361
	 * @param string $mode
362
	 * @return resource|bool
363
	 * @throws GenericEncryptionException
364
	 * @throws ModuleDoesNotExistsException
365
	 */
366
	public function fopen($path, $mode) {
367
368
		// check if the file is stored in the array cache, this means that we
369
		// copy a file over to the versions folder, in this case we don't want to
370
		// decrypt it
371
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
372
			$this->arrayCache->remove('encryption_copy_version_' . $path);
373
			return $this->storage->fopen($path, $mode);
374
		}
375
376
		$encryptionEnabled = $this->encryptionManager->isEnabled();
377
		$shouldEncrypt = false;
378
		$encryptionModule = null;
379
		$header = $this->getHeader($path);
380
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
381
		$fullPath = $this->getFullPath($path);
382
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
383
384
		if ($this->util->isExcluded($fullPath) === false) {
385
386
			$size = $unencryptedSize = 0;
387
			$realFile = $this->util->stripPartialFileExtension($path);
388
			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
389
			$targetIsEncrypted = false;
390
			if ($targetExists) {
391
				// in case the file exists we require the explicit module as
392
				// specified in the file header - otherwise we need to fail hard to
393
				// prevent data loss on client side
394
				if (!empty($encryptionModuleId)) {
395
					$targetIsEncrypted = true;
396
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
397
				}
398
399
				if ($this->file_exists($path)) {
400
					$size = $this->storage->filesize($path);
401
					$unencryptedSize = $this->filesize($path);
402
				} else {
403
					$size = $unencryptedSize = 0;
404
				}
405
			}
406
407
			try {
408
409
				if (
410
					$mode === 'w'
411
					|| $mode === 'w+'
412
					|| $mode === 'wb'
413
					|| $mode === 'wb+'
414
				) {
415
					// don't overwrite encrypted files if encryption is not enabled
416
					if ($targetIsEncrypted && $encryptionEnabled === false) {
417
						throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
418
					}
419
					if ($encryptionEnabled) {
420
						// if $encryptionModuleId is empty, the default module will be used
421
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
422
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
423
						$signed = true;
424
					}
425
				} else {
426
					$info = $this->getCache()->get($path);
427
					// only get encryption module if we found one in the header
428
					// or if file should be encrypted according to the file cache
429
					if (!empty($encryptionModuleId)) {
430
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
431
						$shouldEncrypt = true;
432
					} else if (empty($encryptionModuleId) && $info['encrypted'] === true) {
433
						// we come from a old installation. No header and/or no module defined
434
						// but the file is encrypted. In this case we need to use the
435
						// OC_DEFAULT_MODULE to read the file
436
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
437
						$shouldEncrypt = true;
438
						$targetIsEncrypted = true;
439
					}
440
				}
441
			} catch (ModuleDoesNotExistsException $e) {
442
				$this->logger->warning('Encryption module "' . $encryptionModuleId .
443
					'" not found, file will be stored unencrypted (' . $e->getMessage() . ')');
444
			}
445
446
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
447
			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
448
				if (!$targetExists || !$targetIsEncrypted) {
449
					$shouldEncrypt = false;
450
				}
451
			}
452
453
			if ($shouldEncrypt === true && $encryptionModule !== null) {
454
				$headerSize = $this->getHeaderSize($path);
455
				$source = $this->storage->fopen($path, $mode);
456
				if (!is_resource($source)) {
457
					return false;
458
				}
459
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
460
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
461
					$size, $unencryptedSize, $headerSize, $signed);
462
				return $handle;
463
			}
464
465
		}
466
467
		return $this->storage->fopen($path, $mode);
468
	}
469
470
471
	/**
472
	 * perform some plausibility checks if the the unencrypted size is correct.
473
	 * If not, we calculate the correct unencrypted size and return it
474
	 *
475
	 * @param string $path internal path relative to the storage root
476
	 * @param int $unencryptedSize size of the unencrypted file
477
	 *
478
	 * @return int unencrypted size
479
	 */
480
	protected function verifyUnencryptedSize($path, $unencryptedSize) {
481
482
		$size = $this->storage->filesize($path);
483
		$result = $unencryptedSize;
484
485
		if ($unencryptedSize < 0 ||
486
			($size > 0 && $unencryptedSize === $size)
487
		) {
488
			// check if we already calculate the unencrypted size for the
489
			// given path to avoid recursions
490
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
491
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
492
				try {
493
					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
0 ignored issues
show
Security Bug introduced by
It seems like $size defined by $this->storage->filesize($path) on line 482 can also be of type false; however, OC\Files\Storage\Wrapper...n::fixUnencryptedSize() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
494
				} catch (\Exception $e) {
495
					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
496
					$this->logger->logException($e);
497
				}
498
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
499
			}
500
		}
501
502
		return $result;
503
	}
504
505
	/**
506
	 * calculate the unencrypted size
507
	 *
508
	 * @param string $path internal path relative to the storage root
509
	 * @param int $size size of the physical file
510
	 * @param int $unencryptedSize size of the unencrypted file
511
	 *
512
	 * @return int calculated unencrypted size
513
	 */
514
	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
515
516
		$headerSize = $this->getHeaderSize($path);
517
		$header = $this->getHeader($path);
518
		$encryptionModule = $this->getEncryptionModule($path);
519
520
		$stream = $this->storage->fopen($path, 'r');
521
522
		// if we couldn't open the file we return the old unencrypted size
523
		if (!is_resource($stream)) {
524
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
525
			return $unencryptedSize;
526
		}
527
528
		$newUnencryptedSize = 0;
529
		$size -= $headerSize;
530
		$blockSize = $this->util->getBlockSize();
531
532
		// if a header exists we skip it
533
		if ($headerSize > 0) {
534
			fread($stream, $headerSize);
535
		}
536
537
		// fast path, else the calculation for $lastChunkNr is bogus
538
		if ($size === 0) {
539
			return 0;
540
		}
541
542
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
543
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
544
545
		// calculate last chunk nr
546
		// next highest is end of chunks, one subtracted is last one
547
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
548
549
		$lastChunkNr = ceil($size/ $blockSize)-1;
550
		// calculate last chunk position
551
		$lastChunkPos = ($lastChunkNr * $blockSize);
552
		// try to fseek to the last chunk, if it fails we have to read the whole file
553
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
554
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
555
		}
556
557
		$lastChunkContentEncrypted='';
558
		$count = $blockSize;
559
560
		while ($count > 0) {
561
			$data=fread($stream, $blockSize);
562
			$count=strlen($data);
563
			$lastChunkContentEncrypted .= $data;
564
			if(strlen($lastChunkContentEncrypted) > $blockSize) {
565
				$newUnencryptedSize += $unencryptedBlockSize;
566
				$lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
567
			}
568
		}
569
570
		fclose($stream);
571
572
		// we have to decrypt the last chunk to get it actual size
573
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
574
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
575
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
576
577
		// calc the real file size with the size of the last chunk
578
		$newUnencryptedSize += strlen($decryptedLastChunk);
579
580
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
581
582
		// write to cache if applicable
583
		$cache = $this->storage->getCache();
584
		if ($cache) {
585
			$entry = $cache->get($path);
586
			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
587
		}
588
589
		return $newUnencryptedSize;
590
	}
591
592
	/**
593
	 * @param Storage $sourceStorage
594
	 * @param string $sourceInternalPath
595
	 * @param string $targetInternalPath
596
	 * @param bool $preserveMtime
597
	 * @return bool
598
	 */
599 View Code Duplication
	public function moveFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
600
		if ($sourceStorage === $this) {
601
			return $this->rename($sourceInternalPath, $targetInternalPath);
602
		}
603
604
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
605
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
606
		// - copy the file cache update from  $this->copyBetweenStorage to this method
607
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
608
		// - remove $this->copyBetweenStorage
609
610
		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
611
			return false;
612
		}
613
614
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
615
		if ($result) {
616
			if ($sourceStorage->is_dir($sourceInternalPath)) {
617
				$result &= $sourceStorage->rmdir($sourceInternalPath);
618
			} else {
619
				$result &= $sourceStorage->unlink($sourceInternalPath);
620
			}
621
		}
622
		return $result;
623
	}
624
625
626
	/**
627
	 * @param Storage $sourceStorage
628
	 * @param string $sourceInternalPath
629
	 * @param string $targetInternalPath
630
	 * @param bool $preserveMtime
631
	 * @param bool $isRename
632
	 * @return bool
633
	 */
634
	public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
635
636
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
637
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
638
		// - copy the file cache update from  $this->copyBetweenStorage to this method
639
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
640
		// - remove $this->copyBetweenStorage
641
642
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
643
	}
644
645
	/**
646
	 * Update the encrypted cache version in the database
647
	 *
648
	 * @param Storage $sourceStorage
649
	 * @param string $sourceInternalPath
650
	 * @param string $targetInternalPath
651
	 * @param bool $isRename
652
	 */
653
	private function updateEncryptedVersion(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) {
654
		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath) ? 1 : 0;
655
		$cacheInformation = [
656
			'encrypted' => (bool)$isEncrypted,
657
		];
658
		if($isEncrypted === 1) {
659
			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
660
661
			// In case of a move operation from an unencrypted to an encrypted
662
			// storage the old encrypted version would stay with "0" while the
663
			// correct value would be "1". Thus we manually set the value to "1"
664
			// for those cases.
665
			// See also https://github.com/owncloud/core/issues/23078
666
			if($encryptedVersion === 0) {
667
				$encryptedVersion = 1;
668
			}
669
670
			$cacheInformation['encryptedVersion'] = $encryptedVersion;
671
		}
672
673
		// in case of a rename we need to manipulate the source cache because
674
		// this information will be kept for the new target
675
		if ($isRename) {
676
			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
677
		} else {
678
			$this->getCache()->put($targetInternalPath, $cacheInformation);
679
		}
680
	}
681
682
	/**
683
	 * copy file between two storages
684
	 *
685
	 * @param Storage $sourceStorage
686
	 * @param string $sourceInternalPath
687
	 * @param string $targetInternalPath
688
	 * @param bool $preserveMtime
689
	 * @param bool $isRename
690
	 * @return bool
691
	 * @throws \Exception
692
	 */
693
	private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
694
695
		// for versions we have nothing to do, because versions should always use the
696
		// key from the original file. Just create a 1:1 copy and done
697
		if ($this->isVersion($targetInternalPath) ||
698
			$this->isVersion($sourceInternalPath)) {
699
			// remember that we try to create a version so that we can detect it during
700
			// fopen($sourceInternalPath) and by-pass the encryption in order to
701
			// create a 1:1 copy of the file
702
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
703
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
704
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
705
			if ($result) {
706
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
707
				// make sure that we update the unencrypted size for the version
708
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
709
					$this->updateUnencryptedSize(
710
						$this->getFullPath($targetInternalPath),
711
						$info['size']
712
					);
713
				}
714
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
715
			}
716
			return $result;
717
		}
718
719
		// first copy the keys that we reuse the existing file key on the target location
720
		// and don't create a new one which would break versions for example.
721
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
722
		if (count($mount) === 1) {
723
			$mountPoint = $mount[0]->getMountPoint();
724
			$source = $mountPoint . '/' . $sourceInternalPath;
725
			$target = $this->getFullPath($targetInternalPath);
726
			$this->copyKeys($source, $target);
727
		} else {
728
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
729
		}
730
731
		if ($sourceStorage->is_dir($sourceInternalPath)) {
732
			$dh = $sourceStorage->opendir($sourceInternalPath);
733
			$result = $this->mkdir($targetInternalPath);
734 View Code Duplication
			if (is_resource($dh)) {
735
				while ($result and ($file = readdir($dh)) !== false) {
736
					if (!Filesystem::isIgnoredDir($file)) {
737
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
738
					}
739
				}
740
			}
741
		} else {
742
			try {
743
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
744
				$target = $this->fopen($targetInternalPath, 'w');
745
				list(, $result) = \OC_Helper::streamCopy($source, $target);
0 ignored issues
show
Security Bug introduced by
It seems like $source can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
Security Bug introduced by
It seems like $target can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
746
				fclose($source);
747
				fclose($target);
748
			} catch (\Exception $e) {
749
				fclose($source);
750
				fclose($target);
0 ignored issues
show
Bug introduced by
The variable $target does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
751
				throw $e;
752
			}
753
			if($result) {
754
				if ($preserveMtime) {
755
					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
0 ignored issues
show
Security Bug introduced by
It seems like $sourceStorage->filemtime($sourceInternalPath) targeting OCP\Files\Storage::filemtime() can also be of type false; however, OC\Files\Storage\Wrapper\Wrapper::touch() does only seem to accept integer|null, did you maybe forget to handle an error condition?
Loading history...
756
				}
757
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
758
			} else {
759
				// delete partially written target file
760
				$this->unlink($targetInternalPath);
761
				// delete cache entry that was created by fopen
762
				$this->getCache()->remove($targetInternalPath);
763
			}
764
		}
765
		return (bool)$result;
766
767
	}
768
769
	/**
770
	 * get the path to a local version of the file.
771
	 * The local version of the file can be temporary and doesn't have to be persistent across requests
772
	 *
773
	 * @param string $path
774
	 * @return string
775
	 */
776
	public function getLocalFile($path) {
777
		if ($this->encryptionManager->isEnabled()) {
778
			$cachedFile = $this->getCachedFile($path);
779
			if (is_string($cachedFile)) {
780
				return $cachedFile;
781
			}
782
		}
783
		return $this->storage->getLocalFile($path);
784
	}
785
786
	/**
787
	 * Returns the wrapped storage's value for isLocal()
788
	 *
789
	 * @return bool wrapped storage's isLocal() value
790
	 */
791
	public function isLocal() {
792
		if ($this->encryptionManager->isEnabled()) {
793
			return false;
794
		}
795
		return $this->storage->isLocal();
796
	}
797
798
	/**
799
	 * see http://php.net/manual/en/function.stat.php
800
	 * only the following keys are required in the result: size and mtime
801
	 *
802
	 * @param string $path
803
	 * @return array
804
	 */
805
	public function stat($path) {
806
		$stat = $this->storage->stat($path);
807
		$fileSize = $this->filesize($path);
808
		$stat['size'] = $fileSize;
809
		$stat[7] = $fileSize;
810
		return $stat;
811
	}
812
813
	/**
814
	 * see http://php.net/manual/en/function.hash.php
815
	 *
816
	 * @param string $type
817
	 * @param string $path
818
	 * @param bool $raw
819
	 * @return string
820
	 */
821 View Code Duplication
	public function hash($type, $path, $raw = false) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
822
		$fh = $this->fopen($path, 'rb');
823
		$ctx = hash_init($type);
824
		hash_update_stream($ctx, $fh);
825
		fclose($fh);
826
		return hash_final($ctx, $raw);
827
	}
828
829
	/**
830
	 * return full path, including mount point
831
	 *
832
	 * @param string $path relative to mount point
833
	 * @return string full path including mount point
834
	 */
835
	protected function getFullPath($path) {
836
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
837
	}
838
839
	/**
840
	 * read first block of encrypted file, typically this will contain the
841
	 * encryption header
842
	 *
843
	 * @param string $path
844
	 * @return string
845
	 */
846
	protected function readFirstBlock($path) {
847
		$firstBlock = '';
848
		if ($this->storage->file_exists($path)) {
849
			$handle = $this->storage->fopen($path, 'r');
850
			$firstBlock = fread($handle, $this->util->getHeaderSize());
851
			fclose($handle);
852
		}
853
		return $firstBlock;
854
	}
855
856
	/**
857
	 * return header size of given file
858
	 *
859
	 * @param string $path
860
	 * @return int
861
	 */
862
	protected function getHeaderSize($path) {
863
		$headerSize = 0;
864
		$realFile = $this->util->stripPartialFileExtension($path);
865
		if ($this->storage->file_exists($realFile)) {
866
			$path = $realFile;
867
		}
868
		$firstBlock = $this->readFirstBlock($path);
869
870
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
871
			$headerSize = $this->util->getHeaderSize();
872
		}
873
874
		return $headerSize;
875
	}
876
877
	/**
878
	 * parse raw header to array
879
	 *
880
	 * @param string $rawHeader
881
	 * @return array
882
	 */
883
	protected function parseRawHeader($rawHeader) {
884
		$result = array();
885
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
886
			$header = $rawHeader;
887
			$endAt = strpos($header, Util::HEADER_END);
888
			if ($endAt !== false) {
889
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
890
891
				// +1 to not start with an ':' which would result in empty element at the beginning
892
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
893
894
				$element = array_shift($exploded);
895 View Code Duplication
				while ($element !== Util::HEADER_END) {
896
					$result[$element] = array_shift($exploded);
897
					$element = array_shift($exploded);
898
				}
899
			}
900
		}
901
902
		return $result;
903
	}
904
905
	/**
906
	 * read header from file
907
	 *
908
	 * @param string $path
909
	 * @return array
910
	 */
911
	protected function getHeader($path) {
912
		$realFile = $this->util->stripPartialFileExtension($path);
913
		$exists = $this->storage->file_exists($realFile);
914
		if ($exists) {
915
			$path = $realFile;
916
		}
917
918
		$firstBlock = $this->readFirstBlock($path);
919
		$result = $this->parseRawHeader($firstBlock);
920
921
		// if the header doesn't contain a encryption module we check if it is a
922
		// legacy file. If true, we add the default encryption module
923
		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
924
			if (!empty($result)) {
925
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
926
			} else if ($exists) {
927
				// if the header was empty we have to check first if it is a encrypted file at all
928
				// We would do query to filecache only if we know that entry in filecache exists
929
				$info = $this->getCache()->get($path);
930
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
931
					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
932
				}
933
			}
934
		}
935
936
		return $result;
937
	}
938
939
	/**
940
	 * read encryption module needed to read/write the file located at $path
941
	 *
942
	 * @param string $path
943
	 * @return null|\OCP\Encryption\IEncryptionModule
944
	 * @throws ModuleDoesNotExistsException
945
	 * @throws \Exception
946
	 */
947
	protected function getEncryptionModule($path) {
948
		$encryptionModule = null;
949
		$header = $this->getHeader($path);
950
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
951
		if (!empty($encryptionModuleId)) {
952
			try {
953
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
954
			} catch (ModuleDoesNotExistsException $e) {
955
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
956
				throw $e;
957
			}
958
		}
959
960
		return $encryptionModule;
961
	}
962
963
	/**
964
	 * @param string $path
965
	 * @param int $unencryptedSize
966
	 */
967
	public function updateUnencryptedSize($path, $unencryptedSize) {
968
		$this->unencryptedSize[$path] = $unencryptedSize;
969
	}
970
971
	/**
972
	 * copy keys to new location
973
	 *
974
	 * @param string $source path relative to data/
975
	 * @param string $target path relative to data/
976
	 * @return bool
977
	 */
978
	protected function copyKeys($source, $target) {
979
		if (!$this->util->isExcluded($source)) {
980
			return $this->keyStorage->copyKeys($source, $target);
981
		}
982
983
		return false;
984
	}
985
986
	/**
987
	 * check if path points to a files version
988
	 *
989
	 * @param $path
990
	 * @return bool
991
	 */
992
	protected function isVersion($path) {
993
		$normalized = Filesystem::normalizePath($path);
994
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
995
	}
996
997
	/**
998
	 * check if the given storage should be encrypted or not
999
	 *
1000
	 * @param $path
1001
	 * @return bool
1002
	 */
1003
	protected function shouldEncrypt($path) {
1004
		$fullPath = $this->getFullPath($path);
1005
		$mountPointConfig = $this->mount->getOption('encrypt', true);
1006
		if ($mountPointConfig === false) {
1007
			return false;
1008
		}
1009
1010
		try {
1011
			$encryptionModule = $this->getEncryptionModule($fullPath);
1012
		} catch (ModuleDoesNotExistsException $e) {
1013
			return false;
1014
		}
1015
1016
		if ($encryptionModule === null) {
1017
			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1018
		}
1019
1020
		return $encryptionModule->shouldEncrypt($fullPath);
1021
1022
	}
1023
1024
}
1025