Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

Encryption::readCache()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 0
dl 0
loc 14
rs 9.4888
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Björn Schießle <[email protected]>
4
 * @author jknockaert <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Roeland Jago Douma <[email protected]>
7
 * @author Thomas Müller <[email protected]>
8
 * @author Vincent Petry <[email protected]>
9
 *
10
 * @copyright Copyright (c) 2018, 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\Stream;
28
29
use Icewind\Streams\Wrapper;
30
use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;
31
32
class Encryption extends Wrapper {
33
34
	/** @var \OC\Encryption\Util */
35
	protected $util;
36
37
	/** @var \OC\Encryption\File */
38
	protected $file;
39
40
	/** @var \OCP\Encryption\IEncryptionModule */
41
	protected $encryptionModule;
42
43
	/** @var \OC\Files\Storage\Storage */
44
	protected $storage;
45
46
	/** @var \OC\Files\Storage\Wrapper\Encryption */
47
	protected $encryptionStorage;
48
49
	/** @var string */
50
	protected $internalPath;
51
52
	/** @var string */
53
	protected $cache;
54
55
	/** @var integer */
56
	protected $size;
57
58
	/** @var integer */
59
	protected $position;
60
61
	/** @var integer */
62
	protected $unencryptedSize;
63
64
	/** @var integer */
65
	protected $headerSize;
66
67
	/** @var integer */
68
	protected $unencryptedBlockSize;
69
70
	/** @var array */
71
	protected $header;
72
73
	/** @var string */
74
	protected $fullPath;
75
76
	/** @var  bool */
77
	protected $signed;
78
79
	/**
80
	 * header data returned by the encryption module, will be written to the file
81
	 * in case of a write operation
82
	 *
83
	 * @var array
84
	 */
85
	protected $newHeader;
86
87
	/**
88
	 * user who perform the read/write operation null for public access
89
	 *
90
	 * @var string
91
	 */
92
	protected $uid;
93
94
	/** @var bool */
95
	protected $readOnly;
96
97
	/** @var bool */
98
	protected $writeFlag;
99
100
	/** @var array */
101
	protected $expectedContextProperties;
102
103
	public function __construct() {
104
		$this->expectedContextProperties = [
105
			'source',
106
			'storage',
107
			'internalPath',
108
			'fullPath',
109
			'encryptionModule',
110
			'header',
111
			'uid',
112
			'file',
113
			'util',
114
			'size',
115
			'unencryptedSize',
116
			'encryptionStorage',
117
			'headerSize',
118
			'signed',
119
			'sourceFileOfRename'
120
		];
121
	}
122
123
	/**
124
	 * Wraps a stream with the provided callbacks
125
	 *
126
	 * @param resource $source
127
	 * @param string $internalPath relative to mount point
128
	 * @param string $fullPath relative to data/
129
	 * @param array $header
130
	 * @param string $uid
131
	 * @param \OCP\Encryption\IEncryptionModule $encryptionModule
132
	 * @param \OC\Files\Storage\Storage $storage
133
	 * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
134
	 * @param \OC\Encryption\Util $util
135
	 * @param \OC\Encryption\File $file
136
	 * @param string $mode
137
	 * @param int $size
138
	 * @param int $unencryptedSize
139
	 * @param int $headerSize
140
	 * @param bool $signed
141
	 * @param null|string $sourceFileOfRename
142
	 * @param string $wrapper stream wrapper class
143
	 * @return resource
144
	 *
145
	 * @throws \BadMethodCallException
146
	 */
147
	public static function wrap($source, $internalPath, $fullPath, array $header,
148
								$uid,
149
								\OCP\Encryption\IEncryptionModule $encryptionModule,
150
								\OC\Files\Storage\Storage $storage,
151
								\OC\Files\Storage\Wrapper\Encryption $encStorage,
152
								\OC\Encryption\Util $util,
153
								 \OC\Encryption\File $file,
154
								$mode,
155
								$size,
156
								$unencryptedSize,
157
								$headerSize,
158
								$signed,
159
								$sourceFileOfRename = null,
160
								$wrapper =  'OC\Files\Stream\Encryption') {
161
		$context = \stream_context_create([
162
			'ocencryption' => [
163
				'source' => $source,
164
				'storage' => $storage,
165
				'internalPath' => $internalPath,
166
				'fullPath' => $fullPath,
167
				'encryptionModule' => $encryptionModule,
168
				'header' => $header,
169
				'uid' => $uid,
170
				'util' => $util,
171
				'file' => $file,
172
				'size' => $size,
173
				'unencryptedSize' => $unencryptedSize,
174
				'encryptionStorage' => $encStorage,
175
				'headerSize' => $headerSize,
176
				'signed' => $signed,
177
				'sourceFileOfRename' => $sourceFileOfRename
178
			]
179
		]);
180
181
		return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
182
	}
183
184
	/**
185
	 * add stream wrapper
186
	 *
187
	 * @param resource $source
188
	 * @param string $mode
189
	 * @param resource $context
190
	 * @param string $protocol
191
	 * @param string $class
192
	 * @return resource
193
	 * @throws \BadMethodCallException
194
	 */
195
	protected static function wrapSource($source, $context, $protocol, $class, $mode = 'r+') {
196
		try {
197
			\stream_wrapper_register($protocol, $class);
198
			if (@\rewinddir($source) === false) {
199
				$wrapped = \fopen($protocol . '://', $mode, false, $context);
200
			} else {
201
				$wrapped = \opendir($protocol . '://', $context);
202
			}
203
		} catch (\BadMethodCallException $e) {
204
			\stream_wrapper_unregister($protocol);
205
			throw $e;
206
		}
207
		\stream_wrapper_unregister($protocol);
208
		return $wrapped;
209
	}
210
211
	/**
212
	 * Load the source from the stream context and return the context options
213
	 *
214
	 * @param string $name
215
	 * @return array
216
	 * @throws \BadMethodCallException
217
	 */
218
	protected function loadContext($name) {
219
		$context = parent::loadContext($name);
220
221
		foreach ($this->expectedContextProperties as $property) {
222
			if (\array_key_exists($property, $context)) {
223
				$this->{$property} = $context[$property];
224
			} else {
225
				throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
226
			}
227
		}
228
		return $context;
229
	}
230
231
	public function stream_open($path, $mode, $options, &$opened_path) {
232
		$context = $this->loadContext('ocencryption');
233
234
		$this->position = 0;
235
		$this->cache = '';
236
		$this->writeFlag = false;
237
		$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
238
239
		if (
240
			$mode === 'w'
241
			|| $mode === 'w+'
242
			|| $mode === 'wb'
243
			|| $mode === 'wb+'
244
			|| $mode === 'r+'
245
			|| $mode === 'rb+'
246
		) {
247
			$this->readOnly = false;
248
		} else {
249
			$this->readOnly = true;
250
		}
251
252
		$sharePath = $this->fullPath;
253
		if (!$this->storage->file_exists($this->internalPath)) {
254
			$sharePath = \dirname($sharePath);
255
		}
256
257
		$accessList = $this->file->getAccessList($sharePath);
258
		$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList, $context['sourceFileOfRename']);
259
260
		if (
261
			$mode === 'w'
262
			|| $mode === 'w+'
263
			|| $mode === 'wb'
264
			|| $mode === 'wb+'
265
		) {
266
			// We're writing a new file so start write counter with 0 bytes
267
			$this->unencryptedSize = 0;
268
			$this->writeHeader();
269
			$this->headerSize = $this->util->getHeaderSize();
270
			$this->size = $this->headerSize;
271
		} else {
272
			$this->skipHeader();
273
		}
274
275
		return true;
276
	}
277
278
	public function stream_eof() {
279
		return $this->position >= $this->unencryptedSize;
280
	}
281
282
	public function stream_read($count) {
283
		$result = '';
284
285
		$count = \min($count, $this->unencryptedSize - $this->position);
286
		while ($count > 0) {
287
			$remainingLength = $count;
288
			// update the cache of the current block
289
			$this->readCache();
290
			// determine the relative position in the current block
291
			$blockPosition = ($this->position % $this->unencryptedBlockSize);
292
			// if entire read inside current block then only position needs to be updated
293
			if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
294
				$result .= \substr($this->cache, $blockPosition, $remainingLength);
295
				$this->position += $remainingLength;
296
				$count = 0;
297
			// otherwise remainder of current block is fetched, the block is flushed and the position updated
298
			} else {
299
				$result .= \substr($this->cache, $blockPosition);
300
				$this->flush();
301
				$this->position += ($this->unencryptedBlockSize - $blockPosition);
302
				$count -= ($this->unencryptedBlockSize - $blockPosition);
303
			}
304
		}
305
		return $result;
306
	}
307
308
	public function stream_write($data) {
309
		$length = 0;
310
		// loop over $data to fit it in 6126 sized unencrypted blocks
311
		while (isset($data[0])) {
312
			$remainingLength = \strlen($data);
313
314
			// set the cache to the current 6126 block
315
			$this->readCache();
316
317
			// for seekable streams the pointer is moved back to the beginning of the encrypted block
318
			// flush will start writing there when the position moves to another block
319
			$positionInFile = (int)\floor($this->position / $this->unencryptedBlockSize) *
320
				$this->util->getBlockSize() + $this->headerSize;
321
			$resultFseek = $this->parentStreamSeek($positionInFile);
322
323
			// only allow writes on seekable streams, or at the end of the encrypted stream
324
			if (!($this->readOnly) && ($resultFseek || $positionInFile === $this->size)) {
325
326
				// switch the writeFlag so flush() will write the block
327
				$this->writeFlag = true;
328
329
				// determine the relative position in the current block
330
				$blockPosition = ($this->position % $this->unencryptedBlockSize);
331
				// check if $data fits in current block
332
				// if so, overwrite existing data (if any)
333
				// update position and liberate $data
334
				if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
335
					$this->cache = \substr($this->cache, 0, $blockPosition)
336
						. $data . \substr($this->cache, $blockPosition + $remainingLength);
337
					$this->position += $remainingLength;
338
					$length += $remainingLength;
339
					$data = '';
340
				// if $data doesn't fit the current block, the fill the current block and reiterate
341
					// after the block is filled, it is flushed and $data is updatedxxx
342
				} else {
343
					$this->cache = \substr($this->cache, 0, $blockPosition) .
344
						\substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
345
					$this->flush();
346
					$this->position += ($this->unencryptedBlockSize - $blockPosition);
347
					$length += ($this->unencryptedBlockSize - $blockPosition);
348
					$data = \substr($data, $this->unencryptedBlockSize - $blockPosition);
349
				}
350
			} else {
351
				$data = '';
352
			}
353
			$this->unencryptedSize = \max($this->unencryptedSize, $this->position);
354
		}
355
		return $length;
356
	}
357
358
	public function stream_tell() {
359
		return $this->position;
360
	}
361
362
	public function stream_seek($offset, $whence = SEEK_SET) {
363
		$return = false;
364
365
		switch ($whence) {
366
			case SEEK_SET:
367
				$newPosition = $offset;
368
				break;
369
			case SEEK_CUR:
370
				$newPosition = $this->position + $offset;
371
				break;
372
			case SEEK_END:
373
				$newPosition = $this->unencryptedSize + $offset;
374
				break;
375
			default:
376
				return $return;
377
		}
378
379
		if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
380
			return $return;
381
		}
382
383
		$newFilePosition = \floor($newPosition / $this->unencryptedBlockSize)
384
			* $this->util->getBlockSize() + $this->headerSize;
385
386
		$oldFilePosition = parent::stream_tell();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_tell() instead of stream_seek()). Are you sure this is correct? If so, you might want to change this to $this->stream_tell().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
387
		if ($this->parentStreamSeek($newFilePosition)) {
388
			$this->parentStreamSeek($oldFilePosition);
389
			$this->flush();
390
			$this->parentStreamSeek($newFilePosition);
391
			$this->position = $newPosition;
392
			$return = true;
393
		}
394
		return $return;
395
	}
396
397
	public function stream_close() {
398
		$this->flush('end');
399
		$position = (int)\floor($this->position/$this->unencryptedBlockSize);
400
		$remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
401
		if ($this->readOnly === false) {
402
			if (!empty($remainingData)) {
403
				parent::stream_write($remainingData);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_write() instead of stream_close()). Are you sure this is correct? If so, you might want to change this to $this->stream_write().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
404
			}
405
			$this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
406
		}
407
		return parent::stream_close();
408
	}
409
410
	/**
411
	 * write block to file
412
	 * @param string $positionPrefix
413
	 */
414
	protected function flush($positionPrefix = '') {
415
		// write to disk only when writeFlag was set to 1
416
		if ($this->writeFlag) {
417
			// Disable the file proxies so that encryption is not
418
			// automatically attempted when the file is written to disk -
419
			// we are handling that separately here and we don't want to
420
			// get into an infinite loop
421
			$position = (int)\floor($this->position/$this->unencryptedBlockSize);
422
			$encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
423
			$bytesWritten = parent::stream_write($encrypted);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_write() instead of flush()). Are you sure this is correct? If so, you might want to change this to $this->stream_write().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
424
			$this->writeFlag = false;
425
			// Check whether the write concerns the last block
426
			// If so then update the encrypted filesize
427
			// Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
428
			// We recalculate the encrypted filesize as we do not know the context of calling flush()
429
			$completeBlocksInFile=(int)\floor($this->unencryptedSize/$this->unencryptedBlockSize);
430
			if ($completeBlocksInFile === (int)\floor($this->position/$this->unencryptedBlockSize)) {
431
				$this->size = $this->util->getBlockSize() * $completeBlocksInFile;
432
				$this->size += $bytesWritten;
433
				$this->size += $this->headerSize;
434
			}
435
		}
436
		// always empty the cache (otherwise readCache() will not fill it with the new block)
437
		$this->cache = '';
438
	}
439
440
	/**
441
	 * read block to file
442
	 */
443
	protected function readCache() {
444
		// cache should always be empty string when this function is called
445
		// don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
446
		if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
447
			// Get the data from the file handle
448
			$data = parent::stream_read($this->util->getBlockSize());
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_read() instead of readCache()). Are you sure this is correct? If so, you might want to change this to $this->stream_read().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
449
			$position = (int)\floor($this->position/$this->unencryptedBlockSize);
450
			$numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
451
			if ($numberOfChunks === $position) {
452
				$position .= 'end';
453
			}
454
			$this->cache = $this->encryptionModule->decrypt($data, $position);
455
		}
456
	}
457
458
	/**
459
	 * write header at beginning of encrypted file
460
	 *
461
	 * @return integer
462
	 * @throws EncryptionHeaderKeyExistsException if header key is already in use
463
	 */
464
	protected function writeHeader() {
465
		$header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
466
		return parent::stream_write($header);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_write() instead of writeHeader()). Are you sure this is correct? If so, you might want to change this to $this->stream_write().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
467
	}
468
469
	/**
470
	 * read first block to skip the header
471
	 */
472
	protected function skipHeader() {
473
		parent::stream_read($this->headerSize);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_read() instead of skipHeader()). Are you sure this is correct? If so, you might want to change this to $this->stream_read().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
474
	}
475
476
	/**
477
	 * call stream_seek() from parent class
478
	 *
479
	 * @param integer $position
480
	 * @return bool
481
	 */
482
	protected function parentStreamSeek($position) {
483
		return parent::stream_seek($position);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (stream_seek() instead of parentStreamSeek()). Are you sure this is correct? If so, you might want to change this to $this->stream_seek().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
484
	}
485
486
	/**
487
	 * @param string $path
488
	 * @param array $options
489
	 * @return bool
490
	 */
491
	public function dir_opendir($path, $options) {
492
		return false;
493
	}
494
}
495