Completed
Pull Request — master (#28820)
by Sujith
10:44
created

Encryption::updateEncryptedVersion()   C

Complexity

Conditions 7
Paths 36

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
484
					$size, $unencryptedSize, $headerSize, $signed, $sourceFileOfRename);
0 ignored issues
show
Security Bug introduced by
It seems like $size defined by $this->storage->filesize($path) on line 406 can also be of type false; however, OC\Files\Stream\Encryption::wrap() 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...
485
				unset($this->sourcePath[$path]);
486
487
				return $handle;
488
			}
489
490
		}
491
492
		return $this->storage->fopen($path, $mode);
493
	}
494
495
496
	/**
497
	 * perform some plausibility checks if the the unencrypted size is correct.
498
	 * If not, we calculate the correct unencrypted size and return it
499
	 *
500
	 * @param string $path internal path relative to the storage root
501
	 * @param int $unencryptedSize size of the unencrypted file
502
	 *
503
	 * @return int unencrypted size
504
	 */
505
	protected function verifyUnencryptedSize($path, $unencryptedSize) {
506
507
		$size = $this->storage->filesize($path);
508
		$result = $unencryptedSize;
509
510
		if ($unencryptedSize < 0 ||
511
			($size > 0 && $unencryptedSize === $size)
512
		) {
513
			// check if we already calculate the unencrypted size for the
514
			// given path to avoid recursions
515
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
516
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
517
				try {
518
					$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 507 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...
519
				} catch (\Exception $e) {
520
					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
521
					$this->logger->logException($e);
522
				}
523
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
524
			}
525
		}
526
527
		return $result;
528
	}
529
530
	/**
531
	 * calculate the unencrypted size
532
	 *
533
	 * @param string $path internal path relative to the storage root
534
	 * @param int $size size of the physical file
535
	 * @param int $unencryptedSize size of the unencrypted file
536
	 *
537
	 * @return int calculated unencrypted size
538
	 */
539
	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
540
541
		$headerSize = $this->getHeaderSize($path);
542
		$header = $this->getHeader($path);
543
		$encryptionModule = $this->getEncryptionModule($path);
544
545
		$stream = $this->storage->fopen($path, 'r');
546
547
		// if we couldn't open the file we return the old unencrypted size
548
		if (!is_resource($stream)) {
549
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
550
			return $unencryptedSize;
551
		}
552
553
		$newUnencryptedSize = 0;
554
		$size -= $headerSize;
555
		$blockSize = $this->util->getBlockSize();
556
557
		// if a header exists we skip it
558
		if ($headerSize > 0) {
559
			fread($stream, $headerSize);
560
		}
561
562
		// fast path, else the calculation for $lastChunkNr is bogus
563
		if ($size === 0) {
564
			return 0;
565
		}
566
567
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
568
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
569
570
		// calculate last chunk nr
571
		// next highest is end of chunks, one subtracted is last one
572
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
573
574
		$lastChunkNr = ceil($size/ $blockSize)-1;
575
		// calculate last chunk position
576
		$lastChunkPos = ($lastChunkNr * $blockSize);
577
		// try to fseek to the last chunk, if it fails we have to read the whole file
578
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
579
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
580
		}
581
582
		$lastChunkContentEncrypted='';
583
		$count = $blockSize;
584
585
		while ($count > 0) {
586
			$data=fread($stream, $blockSize);
587
			$count=strlen($data);
588
			$lastChunkContentEncrypted .= $data;
589
			if(strlen($lastChunkContentEncrypted) > $blockSize) {
590
				$newUnencryptedSize += $unencryptedBlockSize;
591
				$lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
592
			}
593
		}
594
595
		fclose($stream);
596
597
		// we have to decrypt the last chunk to get it actual size
598
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, [], null);
599
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
600
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
601
602
		// calc the real file size with the size of the last chunk
603
		$newUnencryptedSize += strlen($decryptedLastChunk);
604
605
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
606
607
		// write to cache if applicable
608
		$cache = $this->storage->getCache();
609
		if ($cache) {
610
			$entry = $cache->get($path);
611
			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
612
		}
613
614
		return $newUnencryptedSize;
615
	}
616
617
	/**
618
	 * @param Storage $sourceStorage
619
	 * @param string $sourceInternalPath
620
	 * @param string $targetInternalPath
621
	 * @param bool $preserveMtime
622
	 * @return bool
623
	 */
624 View Code Duplication
	public function moveFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
625
		if ($sourceStorage === $this) {
626
			return $this->rename($sourceInternalPath, $targetInternalPath);
627
		}
628
629
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
630
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
631
		// - copy the file cache update from  $this->copyBetweenStorage to this method
632
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
633
		// - remove $this->copyBetweenStorage
634
635
		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
636
			return false;
637
		}
638
639
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
640
		if ($result) {
641
			if ($sourceStorage->is_dir($sourceInternalPath)) {
642
				$result &= $sourceStorage->rmdir($sourceInternalPath);
643
			} else {
644
				$result &= $sourceStorage->unlink($sourceInternalPath);
645
			}
646
		}
647
		return $result;
648
	}
649
650
	/**
651
	 * Set the flag to true, so that the file would be
652
	 * in the decrypted state.
653
	 *
654
	 * @param $isDisabled bool
655
	 */
656
	public static function setDisableWriteEncryption($isDisabled) {
657
		self::$disableWriteEncryption = $isDisabled;
658
	}
659
660
	/**
661
	 * @param Storage $sourceStorage
662
	 * @param string $sourceInternalPath
663
	 * @param string $targetInternalPath
664
	 * @param bool $preserveMtime
665
	 * @param bool $isRename
666
	 * @return bool
667
	 */
668
	public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
669
670
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
671
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
672
		// - copy the file cache update from  $this->copyBetweenStorage to this method
673
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
674
		// - remove $this->copyBetweenStorage
675
676
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
677
	}
678
679
	/**
680
	 * Update the encrypted cache version in the database
681
	 *
682
	 * @param Storage $sourceStorage
683
	 * @param string $sourceInternalPath
684
	 * @param string $targetInternalPath
685
	 * @param bool $isRename
686
	 */
687
	private function updateEncryptedVersion(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) {
688
		$isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0;
689
		$cacheInformation = [
690
			'encrypted' => (bool)$isEncrypted,
691
		];
692
		if($isEncrypted === 1) {
693
			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
694
695
			// In case of a move operation from an unencrypted to an encrypted
696
			// storage the old encrypted version would stay with "0" while the
697
			// correct value would be "1". Thus we manually set the value to "1"
698
			// for those cases.
699
			// See also https://github.com/owncloud/core/issues/23078
700
			if($encryptedVersion === 0) {
701
				$encryptedVersion = 1;
702
			}
703
704
			$cacheInformation['encryptedVersion'] = $encryptedVersion;
705
		}
706
707
		// in case of a rename we need to manipulate the source cache because
708
		// this information will be kept for the new target
709
		if ($isRename) {
710
			/*
711
			 * Rename is a process of creating a new file. Here we try to use the
712
			 * incremented version of source file, for the destination file.
713
			 */
714
			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
715
			if ($this->encryptionManager->isEnabled()) {
716
				$cacheInformation['encryptedVersion'] = $encryptedVersion + 1;
717
			}
718
			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
719
		} else {
720
			$this->getCache()->put($targetInternalPath, $cacheInformation);
721
		}
722
	}
723
724
	/**
725
	 * copy file between two storages
726
	 *
727
	 * @param Storage $sourceStorage
728
	 * @param string $sourceInternalPath
729
	 * @param string $targetInternalPath
730
	 * @param bool $preserveMtime
731
	 * @param bool $isRename
732
	 * @return bool
733
	 * @throws \Exception
734
	 */
735
	private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
736
		// for versions we have nothing to do, because versions should always use the
737
		// key from the original file. Just create a 1:1 copy and done
738
		if ($this->isVersion($targetInternalPath) ||
739
			$this->isVersion($sourceInternalPath)) {
740
			// remember that we try to create a version so that we can detect it during
741
			// fopen($sourceInternalPath) and by-pass the encryption in order to
742
			// create a 1:1 copy of the file
743
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
744
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
745
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
746
			if ($result) {
747
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
748
				// make sure that we update the unencrypted size for the version
749
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
750
					$this->updateUnencryptedSize(
751
						$this->getFullPath($targetInternalPath),
752
						$info['size']
753
					);
754
				}
755
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
756
			}
757
			return $result;
758
		}
759
760
		// first copy the keys that we reuse the existing file key on the target location
761
		// and don't create a new one which would break versions for example.
762
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
763
		if (count($mount) === 1) {
764
			$mountPoint = $mount[0]->getMountPoint();
765
			$source = $mountPoint . '/' . $sourceInternalPath;
766
			$target = $this->getFullPath($targetInternalPath);
767
			$this->copyKeys($source, $target);
768
		} else {
769
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
770
		}
771
772
		if ($sourceStorage->is_dir($sourceInternalPath)) {
773
			$dh = $sourceStorage->opendir($sourceInternalPath);
774
			$result = $this->mkdir($targetInternalPath);
775 View Code Duplication
			if (is_resource($dh)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
776
				while ($result and ($file = readdir($dh)) !== false) {
777
					if (!Filesystem::isIgnoredDir($file)) {
778
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
779
					}
780
				}
781
			}
782
		} else {
783
			try {
784
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
785
				if ($isRename) {
786
					$this->sourcePath[$targetInternalPath] = $sourceStorage->getFullPath($sourceInternalPath);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\Files\Storage as the method getFullPath() does only exist in the following implementations of said interface: OC\Files\Storage\Wrapper\Encryption.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
787
				} else {
788
					unset($this->sourcePath[$targetInternalPath]);
789
				}
790
				$target = $this->fopen($targetInternalPath, 'w');
791
				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...
792
				fclose($source);
793
				fclose($target);
794
			} catch (\Exception $e) {
795
				fclose($source);
796
				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...
797
				throw $e;
798
			}
799
			if($result) {
800
				if ($preserveMtime) {
801
					$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...
802
				}
803
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
804
			} else {
805
				// delete partially written target file
806
				$this->unlink($targetInternalPath);
807
				// delete cache entry that was created by fopen
808
				$this->getCache()->remove($targetInternalPath);
809
			}
810
		}
811
		return (bool)$result;
812
813
	}
814
815
	/**
816
	 * get the path to a local version of the file.
817
	 * The local version of the file can be temporary and doesn't have to be persistent across requests
818
	 *
819
	 * @param string $path
820
	 * @return string
821
	 */
822
	public function getLocalFile($path) {
823
		if ($this->encryptionManager->isEnabled()) {
824
			$cachedFile = $this->getCachedFile($path);
825
			if (is_string($cachedFile)) {
826
				return $cachedFile;
827
			}
828
		}
829
		return $this->storage->getLocalFile($path);
830
	}
831
832
	/**
833
	 * Returns the wrapped storage's value for isLocal()
834
	 *
835
	 * @return bool wrapped storage's isLocal() value
836
	 */
837
	public function isLocal() {
838
		if ($this->encryptionManager->isEnabled()) {
839
			return false;
840
		}
841
		return $this->storage->isLocal();
842
	}
843
844
	/**
845
	 * see http://php.net/manual/en/function.stat.php
846
	 * only the following keys are required in the result: size and mtime
847
	 *
848
	 * @param string $path
849
	 * @return array
850
	 */
851
	public function stat($path) {
852
		$stat = $this->storage->stat($path);
853
		$fileSize = $this->filesize($path);
854
		$stat['size'] = $fileSize;
855
		$stat[7] = $fileSize;
856
		return $stat;
857
	}
858
859
	/**
860
	 * see http://php.net/manual/en/function.hash.php
861
	 *
862
	 * @param string $type
863
	 * @param string $path
864
	 * @param bool $raw
865
	 * @return string
866
	 */
867 View Code Duplication
	public function hash($type, $path, $raw = false) {
868
		$fh = $this->fopen($path, 'rb');
869
		$ctx = hash_init($type);
870
		hash_update_stream($ctx, $fh);
871
		fclose($fh);
872
		return hash_final($ctx, $raw);
873
	}
874
875
	/**
876
	 * return full path, including mount point
877
	 *
878
	 * @param string $path relative to mount point
879
	 * @return string full path including mount point
880
	 */
881
	protected function getFullPath($path) {
882
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
883
	}
884
885
	/**
886
	 * read first block of encrypted file, typically this will contain the
887
	 * encryption header
888
	 *
889
	 * @param string $path
890
	 * @return string
891
	 */
892
	protected function readFirstBlock($path) {
893
		$firstBlock = '';
894
		if ($this->storage->file_exists($path)) {
895
			$handle = $this->storage->fopen($path, 'r');
896
			$firstBlock = fread($handle, $this->util->getHeaderSize());
897
			fclose($handle);
898
		}
899
		return $firstBlock;
900
	}
901
902
	/**
903
	 * return header size of given file
904
	 *
905
	 * @param string $path
906
	 * @return int
907
	 */
908
	protected function getHeaderSize($path) {
909
		$headerSize = 0;
910
		$realFile = $this->util->stripPartialFileExtension($path);
911
		if ($this->storage->file_exists($realFile)) {
912
			$path = $realFile;
913
		}
914
		$firstBlock = $this->readFirstBlock($path);
915
916
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
917
			$headerSize = $this->util->getHeaderSize();
918
		}
919
920
		return $headerSize;
921
	}
922
923
	/**
924
	 * parse raw header to array
925
	 *
926
	 * @param string $rawHeader
927
	 * @return array
928
	 */
929
	protected function parseRawHeader($rawHeader) {
930
		$result = [];
931
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
932
			$header = $rawHeader;
933
			$endAt = strpos($header, Util::HEADER_END);
934
			if ($endAt !== false) {
935
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
936
937
				// +1 to not start with an ':' which would result in empty element at the beginning
938
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
939
940
				$element = array_shift($exploded);
941
				while ($element !== Util::HEADER_END) {
942
					$result[$element] = array_shift($exploded);
943
					$element = array_shift($exploded);
944
				}
945
			}
946
		}
947
948
		return $result;
949
	}
950
951
	/**
952
	 * read header from file
953
	 *
954
	 * @param string $path
955
	 * @return array
956
	 */
957
	protected function getHeader($path) {
958
		$realFile = $this->util->stripPartialFileExtension($path);
959
		$exists = $this->storage->file_exists($realFile);
960
		if ($exists) {
961
			$path = $realFile;
962
		}
963
964
		$firstBlock = $this->readFirstBlock($path);
965
		$result = $this->parseRawHeader($firstBlock);
966
967
		// if the header doesn't contain a encryption module we check if it is a
968
		// legacy file. If true, we add the default encryption module
969
		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
970
			if (!empty($result)) {
971
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
972
			} else if ($exists) {
973
				// if the header was empty we have to check first if it is a encrypted file at all
974
				// We would do query to filecache only if we know that entry in filecache exists
975
				$info = $this->getCache()->get($path);
976
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
977
					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
978
				}
979
			}
980
		}
981
982
		return $result;
983
	}
984
985
	/**
986
	 * read encryption module needed to read/write the file located at $path
987
	 *
988
	 * @param string $path
989
	 * @return null|\OCP\Encryption\IEncryptionModule
990
	 * @throws ModuleDoesNotExistsException
991
	 * @throws \Exception
992
	 */
993
	protected function getEncryptionModule($path) {
994
		$encryptionModule = null;
995
		$header = $this->getHeader($path);
996
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
997
		if (!empty($encryptionModuleId)) {
998
			try {
999
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1000
			} catch (ModuleDoesNotExistsException $e) {
1001
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1002
				throw $e;
1003
			}
1004
		}
1005
		return $encryptionModule;
1006
	}
1007
1008
	/**
1009
	 * @param string $path
1010
	 * @param int $unencryptedSize
1011
	 */
1012
	public function updateUnencryptedSize($path, $unencryptedSize) {
1013
		$this->unencryptedSize[$path] = $unencryptedSize;
1014
	}
1015
1016
	/**
1017
	 * copy keys to new location
1018
	 *
1019
	 * @param string $source path relative to data/
1020
	 * @param string $target path relative to data/
1021
	 * @return bool
1022
	 */
1023
	protected function copyKeys($source, $target) {
1024
		if (!$this->util->isExcluded($source)) {
1025
			return $this->keyStorage->copyKeys($source, $target);
1026
		}
1027
1028
		return false;
1029
	}
1030
1031
	/**
1032
	 * check if path points to a files version
1033
	 *
1034
	 * @param $path
1035
	 * @return bool
1036
	 */
1037
	protected function isVersion($path) {
1038
		$normalized = Filesystem::normalizePath($path);
1039
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1040
	}
1041
1042
}
1043