Passed
Push — master ( 50d522...35aa34 )
by Christoph
43:03 queued 07:41
created

Encryption   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 519
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 231
dl 0
loc 519
rs 3.44
c 0
b 0
f 0
wmc 62

19 Methods

Rating   Name   Duplication   Size   Complexity  
A writeHeader() 0 3 1
A loadContext() 0 11 3
A wrap() 0 33 1
A flush() 0 24 3
A __construct() 0 16 1
B stream_write() 0 49 6
A buildContext() 0 7 2
A skipHeader() 0 2 1
A stream_read_block() 0 12 3
C stream_open() 0 49 13
A dir_opendir() 0 2 1
A stream_close() 0 22 5
A wrapSource() 0 19 4
A stream_eof() 0 2 1
A parentStreamSeek() 0 2 1
A stream_read() 0 24 3
A stream_tell() 0 2 1
B stream_seek() 0 33 7
A readCache() 0 12 5

How to fix   Complexity   

Complex Class

Complex classes like Encryption often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Encryption, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author jknockaert <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author martink-p <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\Files\Stream;
34
35
use Icewind\Streams\Wrapper;
36
use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;
37
use function is_array;
38
use function stream_context_create;
39
40
class Encryption extends Wrapper {
41
42
	/** @var \OC\Encryption\Util */
43
	protected $util;
44
45
	/** @var \OC\Encryption\File */
46
	protected $file;
47
48
	/** @var \OCP\Encryption\IEncryptionModule */
49
	protected $encryptionModule;
50
51
	/** @var \OC\Files\Storage\Storage */
52
	protected $storage;
53
54
	/** @var \OC\Files\Storage\Wrapper\Encryption */
55
	protected $encryptionStorage;
56
57
	/** @var string */
58
	protected $internalPath;
59
60
	/** @var string */
61
	protected $cache;
62
63
	/** @var integer */
64
	protected $size;
65
66
	/** @var integer */
67
	protected $position;
68
69
	/** @var integer */
70
	protected $unencryptedSize;
71
72
	/** @var integer */
73
	protected $headerSize;
74
75
	/** @var integer */
76
	protected $unencryptedBlockSize;
77
78
	/** @var array */
79
	protected $header;
80
81
	/** @var string */
82
	protected $fullPath;
83
84
	/** @var  bool */
85
	protected $signed;
86
87
	/**
88
	 * header data returned by the encryption module, will be written to the file
89
	 * in case of a write operation
90
	 *
91
	 * @var array
92
	 */
93
	protected $newHeader;
94
95
	/**
96
	 * user who perform the read/write operation null for public access
97
	 *
98
	 * @var string
99
	 */
100
	protected $uid;
101
102
	/** @var bool */
103
	protected $readOnly;
104
105
	/** @var bool */
106
	protected $writeFlag;
107
108
	/** @var array */
109
	protected $expectedContextProperties;
110
111
	/** @var bool */
112
	protected $fileUpdated;
113
114
	public function __construct() {
115
		$this->expectedContextProperties = [
116
			'source',
117
			'storage',
118
			'internalPath',
119
			'fullPath',
120
			'encryptionModule',
121
			'header',
122
			'uid',
123
			'file',
124
			'util',
125
			'size',
126
			'unencryptedSize',
127
			'encryptionStorage',
128
			'headerSize',
129
			'signed'
130
		];
131
	}
132
133
134
	/**
135
	 * Wraps a stream with the provided callbacks
136
	 *
137
	 * @param resource $source
138
	 * @param string $internalPath relative to mount point
139
	 * @param string $fullPath relative to data/
140
	 * @param array $header
141
	 * @param string $uid
142
	 * @param \OCP\Encryption\IEncryptionModule $encryptionModule
143
	 * @param \OC\Files\Storage\Storage $storage
144
	 * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
145
	 * @param \OC\Encryption\Util $util
146
	 * @param \OC\Encryption\File $file
147
	 * @param string $mode
148
	 * @param int $size
149
	 * @param int $unencryptedSize
150
	 * @param int $headerSize
151
	 * @param bool $signed
152
	 * @param string $wrapper stream wrapper class
153
	 * @return resource
154
	 *
155
	 * @throws \BadMethodCallException
156
	 */
157
	public static function wrap($source, $internalPath, $fullPath, array $header,
158
								$uid,
159
								\OCP\Encryption\IEncryptionModule $encryptionModule,
160
								\OC\Files\Storage\Storage $storage,
161
								\OC\Files\Storage\Wrapper\Encryption $encStorage,
162
								\OC\Encryption\Util $util,
163
								 \OC\Encryption\File $file,
164
								$mode,
165
								$size,
166
								$unencryptedSize,
167
								$headerSize,
168
								$signed,
169
								$wrapper = Encryption::class) {
170
		$context = stream_context_create([
171
			'ocencryption' => [
172
				'source' => $source,
173
				'storage' => $storage,
174
				'internalPath' => $internalPath,
175
				'fullPath' => $fullPath,
176
				'encryptionModule' => $encryptionModule,
177
				'header' => $header,
178
				'uid' => $uid,
179
				'util' => $util,
180
				'file' => $file,
181
				'size' => $size,
182
				'unencryptedSize' => $unencryptedSize,
183
				'encryptionStorage' => $encStorage,
184
				'headerSize' => $headerSize,
185
				'signed' => $signed
186
			]
187
		]);
188
189
		return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
190
	}
191
192
	/**
193
	 * add stream wrapper
194
	 *
195
	 * @param resource|int $source
196
	 * @param resource|array $context
197
	 * @param string|null $protocol
198
	 * @param string|null $class
199
	 * @param string $mode
200
	 * @return resource
201
	 * @throws \BadMethodCallException
202
	 */
203
	protected static function wrapSource($source, $context = [], $protocol = null, $class = null, $mode = 'r+') {
204
		try {
205
			if ($protocol === null) {
206
				$protocol = self::getProtocol($class);
207
			}
208
209
			stream_wrapper_register($protocol, $class);
210
			$context = self::buildContext($protocol, $context, $source);
211
			if (self::isDirectoryHandle($source)) {
212
				$wrapped = opendir($protocol . '://', $context);
213
			} else {
214
				$wrapped = fopen($protocol . '://', $mode, false, $context);
215
			}
216
		} catch (\BadMethodCallException $e) {
217
			stream_wrapper_unregister($protocol);
218
			throw $e;
219
		}
220
		stream_wrapper_unregister($protocol);
221
		return $wrapped;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $wrapped could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
222
	}
223
224
	/**
225
	 * @todo this is a copy of \Icewind\Streams\WrapperHandler::buildContext -> combine to one shared method?
226
	 */
227
	private static function buildContext($protocol, $context, $source) {
228
		if (is_array($context)) {
229
			$context['source'] = $source;
230
			return stream_context_create([$protocol => $context]);
231
		}
232
233
		return $context;
234
	}
235
236
	/**
237
	 * Load the source from the stream context and return the context options
238
	 *
239
	 * @param string|null $name
240
	 * @return array
241
	 * @throws \BadMethodCallException
242
	 */
243
	protected function loadContext($name = null) {
244
		$context = parent::loadContext($name);
245
246
		foreach ($this->expectedContextProperties as $property) {
247
			if (array_key_exists($property, $context)) {
248
				$this->{$property} = $context[$property];
249
			} else {
250
				throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
251
			}
252
		}
253
		return $context;
254
	}
255
256
	public function stream_open($path, $mode, $options, &$opened_path) {
257
		$this->loadContext('ocencryption');
258
259
		$this->position = 0;
260
		$this->cache = '';
261
		$this->writeFlag = false;
262
		$this->fileUpdated = false;
263
		$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
264
265
		if (
266
			$mode === 'w'
267
			|| $mode === 'w+'
268
			|| $mode === 'wb'
269
			|| $mode === 'wb+'
270
			|| $mode === 'r+'
271
			|| $mode === 'rb+'
272
		) {
273
			$this->readOnly = false;
274
		} else {
275
			$this->readOnly = true;
276
		}
277
278
		$sharePath = $this->fullPath;
279
		if (!$this->storage->file_exists($this->internalPath)) {
280
			$sharePath = dirname($sharePath);
281
		}
282
283
		$accessList = [];
284
		if ($this->encryptionModule->needDetailedAccessList()) {
285
			$accessList = $this->file->getAccessList($sharePath);
286
		}
287
		$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
288
289
		if (
290
			$mode === 'w'
291
			|| $mode === 'w+'
292
			|| $mode === 'wb'
293
			|| $mode === 'wb+'
294
		) {
295
			// We're writing a new file so start write counter with 0 bytes
296
			$this->unencryptedSize = 0;
297
			$this->writeHeader();
298
			$this->headerSize = $this->util->getHeaderSize();
299
			$this->size = $this->headerSize;
300
		} else {
301
			$this->skipHeader();
302
		}
303
304
		return true;
305
	}
306
307
	public function stream_eof() {
308
		return $this->position >= $this->unencryptedSize;
309
	}
310
311
	public function stream_read($count) {
312
		$result = '';
313
314
		$count = min($count, $this->unencryptedSize - $this->position);
315
		while ($count > 0) {
316
			$remainingLength = $count;
317
			// update the cache of the current block
318
			$this->readCache();
319
			// determine the relative position in the current block
320
			$blockPosition = ($this->position % $this->unencryptedBlockSize);
321
			// if entire read inside current block then only position needs to be updated
322
			if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
323
				$result .= substr($this->cache, $blockPosition, $remainingLength);
324
				$this->position += $remainingLength;
325
				$count = 0;
326
			// otherwise remainder of current block is fetched, the block is flushed and the position updated
327
			} else {
328
				$result .= substr($this->cache, $blockPosition);
329
				$this->flush();
330
				$this->position += ($this->unencryptedBlockSize - $blockPosition);
331
				$count -= ($this->unencryptedBlockSize - $blockPosition);
332
			}
333
		}
334
		return $result;
335
	}
336
337
	/**
338
	 * stream_read_block
339
	 *
340
	 * This function is a wrapper for function stream_read.
341
	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
342
	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
343
	 * remote storage over the internet and it does not care about the given $blockSize.
344
	 *
345
	 * @param int $blockSize Length of requested data block in bytes
346
	 * @return string Data fetched from stream.
347
	 */
348
	private function stream_read_block(int $blockSize): string {
349
		$remaining = $blockSize;
350
		$data = '';
351
352
		do {
353
			$chunk = parent::stream_read($remaining);
354
			$chunk_len = strlen($chunk);
355
			$data .= $chunk;
356
			$remaining -= $chunk_len;
357
		} while (($remaining > 0) && ($chunk_len > 0));
358
359
		return $data;
360
	}
361
362
	public function stream_write($data) {
363
		$length = 0;
364
		// loop over $data to fit it in 6126 sized unencrypted blocks
365
		while (isset($data[0])) {
366
			$remainingLength = strlen($data);
367
368
			// set the cache to the current 6126 block
369
			$this->readCache();
370
371
			// for seekable streams the pointer is moved back to the beginning of the encrypted block
372
			// flush will start writing there when the position moves to another block
373
			$positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
374
				$this->util->getBlockSize() + $this->headerSize;
375
			$resultFseek = $this->parentStreamSeek($positionInFile);
376
377
			// only allow writes on seekable streams, or at the end of the encrypted stream
378
			if (!$this->readOnly && ($resultFseek || $positionInFile === $this->size)) {
379
380
				// switch the writeFlag so flush() will write the block
381
				$this->writeFlag = true;
382
				$this->fileUpdated = true;
383
384
				// determine the relative position in the current block
385
				$blockPosition = ($this->position % $this->unencryptedBlockSize);
386
				// check if $data fits in current block
387
				// if so, overwrite existing data (if any)
388
				// update position and liberate $data
389
				if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
390
					$this->cache = substr($this->cache, 0, $blockPosition)
391
						. $data . substr($this->cache, $blockPosition + $remainingLength);
392
					$this->position += $remainingLength;
393
					$length += $remainingLength;
394
					$data = '';
395
				// if $data doesn't fit the current block, the fill the current block and reiterate
396
					// after the block is filled, it is flushed and $data is updatedxxx
397
				} else {
398
					$this->cache = substr($this->cache, 0, $blockPosition) .
399
						substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
400
					$this->flush();
401
					$this->position += ($this->unencryptedBlockSize - $blockPosition);
402
					$length += ($this->unencryptedBlockSize - $blockPosition);
403
					$data = substr($data, $this->unencryptedBlockSize - $blockPosition);
404
				}
405
			} else {
406
				$data = '';
407
			}
408
			$this->unencryptedSize = max($this->unencryptedSize, $this->position);
409
		}
410
		return $length;
411
	}
412
413
	public function stream_tell() {
414
		return $this->position;
415
	}
416
417
	public function stream_seek($offset, $whence = SEEK_SET) {
418
		$return = false;
419
420
		switch ($whence) {
421
			case SEEK_SET:
422
				$newPosition = $offset;
423
				break;
424
			case SEEK_CUR:
425
				$newPosition = $this->position + $offset;
426
				break;
427
			case SEEK_END:
428
				$newPosition = $this->unencryptedSize + $offset;
429
				break;
430
			default:
431
				return $return;
432
		}
433
434
		if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
435
			return $return;
436
		}
437
438
		$newFilePosition = (int)floor($newPosition / $this->unencryptedBlockSize)
439
			* $this->util->getBlockSize() + $this->headerSize;
440
441
		$oldFilePosition = parent::stream_tell();
442
		if ($this->parentStreamSeek($newFilePosition)) {
443
			$this->parentStreamSeek($oldFilePosition);
444
			$this->flush();
445
			$this->parentStreamSeek($newFilePosition);
446
			$this->position = $newPosition;
447
			$return = true;
448
		}
449
		return $return;
450
	}
451
452
	public function stream_close() {
453
		$this->flush('end');
454
		$position = (int)floor($this->position / $this->unencryptedBlockSize);
455
		$remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
456
		if ($this->readOnly === false) {
457
			if (!empty($remainingData)) {
458
				parent::stream_write($remainingData);
459
			}
460
			$this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
461
		}
462
		$result = parent::stream_close();
463
464
		if ($this->fileUpdated) {
465
			$cache = $this->storage->getCache();
466
			$cacheEntry = $cache->get($this->internalPath);
467
			if ($cacheEntry) {
468
				$version = $cacheEntry['encryptedVersion'] + 1;
469
				$cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
470
			}
471
		}
472
473
		return $result;
474
	}
475
476
	/**
477
	 * write block to file
478
	 * @param string $positionPrefix
479
	 */
480
	protected function flush($positionPrefix = '') {
481
		// write to disk only when writeFlag was set to 1
482
		if ($this->writeFlag) {
483
			// Disable the file proxies so that encryption is not
484
			// automatically attempted when the file is written to disk -
485
			// we are handling that separately here and we don't want to
486
			// get into an infinite loop
487
			$position = (int)floor($this->position / $this->unencryptedBlockSize);
488
			$encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
489
			$bytesWritten = parent::stream_write($encrypted);
490
			$this->writeFlag = false;
491
			// Check whether the write concerns the last block
492
			// If so then update the encrypted filesize
493
			// Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
494
			// We recalculate the encrypted filesize as we do not know the context of calling flush()
495
			$completeBlocksInFile = (int)floor($this->unencryptedSize / $this->unencryptedBlockSize);
496
			if ($completeBlocksInFile === (int)floor($this->position / $this->unencryptedBlockSize)) {
497
				$this->size = $this->util->getBlockSize() * $completeBlocksInFile;
498
				$this->size += $bytesWritten;
499
				$this->size += $this->headerSize;
500
			}
501
		}
502
		// always empty the cache (otherwise readCache() will not fill it with the new block)
503
		$this->cache = '';
504
	}
505
506
	/**
507
	 * read block to file
508
	 */
509
	protected function readCache() {
510
		// cache should always be empty string when this function is called
511
		// don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
512
		if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
513
			// Get the data from the file handle
514
			$data = $this->stream_read_block($this->util->getBlockSize());
515
			$position = (int)floor($this->position / $this->unencryptedBlockSize);
516
			$numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
517
			if ($numberOfChunks === $position) {
518
				$position .= 'end';
519
			}
520
			$this->cache = $this->encryptionModule->decrypt($data, $position);
521
		}
522
	}
523
524
	/**
525
	 * write header at beginning of encrypted file
526
	 *
527
	 * @return integer
528
	 * @throws EncryptionHeaderKeyExistsException if header key is already in use
529
	 */
530
	protected function writeHeader() {
531
		$header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
532
		return parent::stream_write($header);
533
	}
534
535
	/**
536
	 * read first block to skip the header
537
	 */
538
	protected function skipHeader() {
539
		$this->stream_read_block($this->headerSize);
540
	}
541
542
	/**
543
	 * call stream_seek() from parent class
544
	 *
545
	 * @param integer $position
546
	 * @return bool
547
	 */
548
	protected function parentStreamSeek($position) {
549
		return parent::stream_seek($position);
550
	}
551
552
	/**
553
	 * @param string $path
554
	 * @param array $options
555
	 * @return bool
556
	 */
557
	public function dir_opendir($path, $options) {
558
		return false;
559
	}
560
}
561