Passed
Push — master ( 0185aa...d14aed )
by Roeland
19:15 queued 08:18
created

Encryption::stream_write()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 28
nc 4
nop 1
dl 0
loc 49
rs 8.8497
c 0
b 0
f 0
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 jknockaert <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Roeland Jago Douma <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Vincent Petry <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OC\Files\Stream;
30
31
use Icewind\Streams\Wrapper;
32
use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;
33
34
class Encryption extends Wrapper {
35
36
	/** @var \OC\Encryption\Util */
37
	protected $util;
38
39
	/** @var \OC\Encryption\File */
40
	protected $file;
41
42
	/** @var \OCP\Encryption\IEncryptionModule */
43
	protected $encryptionModule;
44
45
	/** @var \OC\Files\Storage\Storage */
46
	protected $storage;
47
48
	/** @var \OC\Files\Storage\Wrapper\Encryption */
49
	protected $encryptionStorage;
50
51
	/** @var string */
52
	protected $internalPath;
53
54
	/** @var string */
55
	protected $cache;
56
57
	/** @var integer */
58
	protected $size;
59
60
	/** @var integer */
61
	protected $position;
62
63
	/** @var integer */
64
	protected $unencryptedSize;
65
66
	/** @var integer */
67
	protected $headerSize;
68
69
	/** @var integer */
70
	protected $unencryptedBlockSize;
71
72
	/** @var array */
73
	protected $header;
74
75
	/** @var string */
76
	protected $fullPath;
77
78
	/** @var  bool */
79
	protected $signed;
80
81
	/**
82
	 * header data returned by the encryption module, will be written to the file
83
	 * in case of a write operation
84
	 *
85
	 * @var array
86
	 */
87
	protected $newHeader;
88
89
	/**
90
	 * user who perform the read/write operation null for public access
91
	 *
92
	 * @var string
93
	 */
94
	protected $uid;
95
96
	/** @var bool */
97
	protected $readOnly;
98
99
	/** @var bool */
100
	protected $writeFlag;
101
102
	/** @var array */
103
	protected $expectedContextProperties;
104
105
	/** @var bool */
106
	protected $fileUpdated;
107
108
	public function __construct() {
109
		$this->expectedContextProperties = array(
110
			'source',
111
			'storage',
112
			'internalPath',
113
			'fullPath',
114
			'encryptionModule',
115
			'header',
116
			'uid',
117
			'file',
118
			'util',
119
			'size',
120
			'unencryptedSize',
121
			'encryptionStorage',
122
			'headerSize',
123
			'signed'
124
		);
125
	}
126
127
128
	/**
129
	 * Wraps a stream with the provided callbacks
130
	 *
131
	 * @param resource $source
132
	 * @param string $internalPath relative to mount point
133
	 * @param string $fullPath relative to data/
134
	 * @param array $header
135
	 * @param string $uid
136
	 * @param \OCP\Encryption\IEncryptionModule $encryptionModule
137
	 * @param \OC\Files\Storage\Storage $storage
138
	 * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
139
	 * @param \OC\Encryption\Util $util
140
	 * @param \OC\Encryption\File $file
141
	 * @param string $mode
142
	 * @param int $size
143
	 * @param int $unencryptedSize
144
	 * @param int $headerSize
145
	 * @param bool $signed
146
	 * @param string $wrapper stream wrapper class
147
	 * @return resource
148
	 *
149
	 * @throws \BadMethodCallException
150
	 */
151
	public static function wrap($source, $internalPath, $fullPath, array $header,
152
								$uid,
153
								\OCP\Encryption\IEncryptionModule $encryptionModule,
154
								\OC\Files\Storage\Storage $storage,
155
								\OC\Files\Storage\Wrapper\Encryption $encStorage,
156
								\OC\Encryption\Util $util,
157
								 \OC\Encryption\File $file,
158
								$mode,
159
								$size,
160
								$unencryptedSize,
161
								$headerSize,
162
								$signed,
163
								$wrapper = Encryption::class) {
164
165
		$context = stream_context_create(array(
166
			'ocencryption' => array(
167
				'source' => $source,
168
				'storage' => $storage,
169
				'internalPath' => $internalPath,
170
				'fullPath' => $fullPath,
171
				'encryptionModule' => $encryptionModule,
172
				'header' => $header,
173
				'uid' => $uid,
174
				'util' => $util,
175
				'file' => $file,
176
				'size' => $size,
177
				'unencryptedSize' => $unencryptedSize,
178
				'encryptionStorage' => $encStorage,
179
				'headerSize' => $headerSize,
180
				'signed' => $signed
181
			)
182
		));
183
184
		return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
185
	}
186
187
	/**
188
	 * add stream wrapper
189
	 *
190
	 * @param resource $source
191
	 * @param string $mode
192
	 * @param resource $context
193
	 * @param string $protocol
194
	 * @param string $class
195
	 * @return resource
196
	 * @throws \BadMethodCallException
197
	 */
198
	protected static function wrapSource($source, $context, $protocol, $class, $mode = 'r+') {
199
		try {
200
			stream_wrapper_register($protocol, $class);
201
			if (self::isDirectoryHandle($source)) {
202
				$wrapped = opendir($protocol . '://', $context);
203
			} else {
204
				$wrapped = fopen($protocol . '://', $mode, false, $context);
205
			}
206
		} catch (\BadMethodCallException $e) {
207
			stream_wrapper_unregister($protocol);
208
			throw $e;
209
		}
210
		stream_wrapper_unregister($protocol);
211
		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...
212
	}
213
214
	/**
215
	 * Load the source from the stream context and return the context options
216
	 *
217
	 * @param string $name
218
	 * @return array
219
	 * @throws \BadMethodCallException
220
	 */
221
	protected function loadContext($name) {
222
		$context = parent::loadContext($name);
223
224
		foreach ($this->expectedContextProperties as $property) {
225
			if (array_key_exists($property, $context)) {
226
				$this->{$property} = $context[$property];
227
			} else {
228
				throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
229
			}
230
		}
231
		return $context;
232
233
	}
234
235
	public function stream_open($path, $mode, $options, &$opened_path) {
236
		$this->loadContext('ocencryption');
237
238
		$this->position = 0;
239
		$this->cache = '';
240
		$this->writeFlag = false;
241
		$this->fileUpdated = false;
242
		$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
243
244
		if (
245
			$mode === 'w'
246
			|| $mode === 'w+'
247
			|| $mode === 'wb'
248
			|| $mode === 'wb+'
249
			|| $mode === 'r+'
250
			|| $mode === 'rb+'
251
		) {
252
			$this->readOnly = false;
253
		} else {
254
			$this->readOnly = true;
255
		}
256
257
		$sharePath = $this->fullPath;
258
		if (!$this->storage->file_exists($this->internalPath)) {
259
			$sharePath = dirname($sharePath);
260
		}
261
262
		$accessList = [];
263
		if ($this->encryptionModule->needDetailedAccessList()) {
264
			$accessList = $this->file->getAccessList($sharePath);
265
		}
266
		$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
267
268
		if (
269
			$mode === 'w'
270
			|| $mode === 'w+'
271
			|| $mode === 'wb'
272
			|| $mode === 'wb+'
273
		) {
274
			// We're writing a new file so start write counter with 0 bytes
275
			$this->unencryptedSize = 0;
276
			$this->writeHeader();
277
			$this->headerSize = $this->util->getHeaderSize();
278
			$this->size = $this->headerSize;
279
		} else {
280
			$this->skipHeader();
281
		}
282
283
		return true;
284
285
	}
286
287
	public function stream_eof() {
288
		return $this->position >= $this->unencryptedSize;
289
	}
290
291
	public function stream_read($count) {
292
293
		$result = '';
294
295
		$count = min($count, $this->unencryptedSize - $this->position);
296
		while ($count > 0) {
297
			$remainingLength = $count;
298
			// update the cache of the current block
299
			$this->readCache();
300
			// determine the relative position in the current block
301
			$blockPosition = ($this->position % $this->unencryptedBlockSize);
302
			// if entire read inside current block then only position needs to be updated
303
			if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
304
				$result .= substr($this->cache, $blockPosition, $remainingLength);
305
				$this->position += $remainingLength;
306
				$count = 0;
307
				// otherwise remainder of current block is fetched, the block is flushed and the position updated
308
			} else {
309
				$result .= substr($this->cache, $blockPosition);
310
				$this->flush();
311
				$this->position += ($this->unencryptedBlockSize - $blockPosition);
312
				$count -= ($this->unencryptedBlockSize - $blockPosition);
313
			}
314
		}
315
		return $result;
316
317
	}
318
	
319
	/**
320
	 * stream_read_block
321
	 *
322
	 * This function is a wrapper for function stream_read.
323
	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
324
	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
325
	 * remote storage over the internet and it does not care about the given $blockSize.
326
	 *
327
	 * @param int $blockSize Length of requested data block in bytes
328
	 * @return string Data fetched from stream.
329
	 */
330
	private function stream_read_block(int $blockSize): string {
331
		$remaining = $blockSize;
332
		$data = '';
333
334
		do {
335
			$chunk = parent::stream_read($remaining);
336
			$chunk_len = strlen($chunk);
337
			$data .= $chunk;
338
			$remaining -= $chunk_len;
339
		} while (($remaining > 0) && ($chunk_len > 0));
340
341
		return $data;
342
	}
343
344
	public function stream_write($data) {
345
		$length = 0;
346
		// loop over $data to fit it in 6126 sized unencrypted blocks
347
		while (isset($data[0])) {
348
			$remainingLength = strlen($data);
349
350
			// set the cache to the current 6126 block
351
			$this->readCache();
352
353
			// for seekable streams the pointer is moved back to the beginning of the encrypted block
354
			// flush will start writing there when the position moves to another block
355
			$positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
356
				$this->util->getBlockSize() + $this->headerSize;
357
			$resultFseek = $this->parentStreamSeek($positionInFile);
358
359
			// only allow writes on seekable streams, or at the end of the encrypted stream
360
			if (!$this->readOnly && ($resultFseek || $positionInFile === $this->size)) {
361
362
				// switch the writeFlag so flush() will write the block
363
				$this->writeFlag = true;
364
				$this->fileUpdated = true;
365
366
				// determine the relative position in the current block
367
				$blockPosition = ($this->position % $this->unencryptedBlockSize);
368
				// check if $data fits in current block
369
				// if so, overwrite existing data (if any)
370
				// update position and liberate $data
371
				if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
372
					$this->cache = substr($this->cache, 0, $blockPosition)
373
						. $data . substr($this->cache, $blockPosition + $remainingLength);
374
					$this->position += $remainingLength;
375
					$length += $remainingLength;
376
					$data = '';
377
					// if $data doesn't fit the current block, the fill the current block and reiterate
378
					// after the block is filled, it is flushed and $data is updatedxxx
379
				} else {
380
					$this->cache = substr($this->cache, 0, $blockPosition) .
381
						substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
382
					$this->flush();
383
					$this->position += ($this->unencryptedBlockSize - $blockPosition);
384
					$length += ($this->unencryptedBlockSize - $blockPosition);
385
					$data = substr($data, $this->unencryptedBlockSize - $blockPosition);
386
				}
387
			} else {
388
				$data = '';
389
			}
390
			$this->unencryptedSize = max($this->unencryptedSize, $this->position);
391
		}
392
		return $length;
393
	}
394
395
	public function stream_tell() {
396
		return $this->position;
397
	}
398
399
	public function stream_seek($offset, $whence = SEEK_SET) {
400
401
		$return = false;
402
403
		switch ($whence) {
404
			case SEEK_SET:
405
				$newPosition = $offset;
406
				break;
407
			case SEEK_CUR:
408
				$newPosition = $this->position + $offset;
409
				break;
410
			case SEEK_END:
411
				$newPosition = $this->unencryptedSize + $offset;
412
				break;
413
			default:
414
				return $return;
415
		}
416
417
		if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
418
			return $return;
419
		}
420
421
		$newFilePosition = floor($newPosition / $this->unencryptedBlockSize)
422
			* $this->util->getBlockSize() + $this->headerSize;
423
424
		$oldFilePosition = parent::stream_tell();
425
		if ($this->parentStreamSeek($newFilePosition)) {
0 ignored issues
show
Bug introduced by
$newFilePosition of type double is incompatible with the type integer expected by parameter $position of OC\Files\Stream\Encryption::parentStreamSeek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

425
		if ($this->parentStreamSeek(/** @scrutinizer ignore-type */ $newFilePosition)) {
Loading history...
426
			$this->parentStreamSeek($oldFilePosition);
427
			$this->flush();
428
			$this->parentStreamSeek($newFilePosition);
429
			$this->position = $newPosition;
430
			$return = true;
431
		}
432
		return $return;
433
434
	}
435
436
	public function stream_close() {
437
		$this->flush('end');
438
		$position = (int)floor($this->position/$this->unencryptedBlockSize);
439
		$remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
440
		if ($this->readOnly === false) {
441
			if(!empty($remainingData)) {
442
				parent::stream_write($remainingData);
443
			}
444
			$this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
445
		}
446
		$result = parent::stream_close();
447
448
		if ($this->fileUpdated) {
449
			$cache = $this->storage->getCache();
450
			$cacheEntry = $cache->get($this->internalPath);
451
			if ($cacheEntry) {
452
				$version = $cacheEntry['encryptedVersion'] + 1;
453
				$cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
454
			}
455
		}
456
457
		return $result;
458
	}
459
460
	/**
461
	 * write block to file
462
	 * @param string $positionPrefix
463
	 */
464
	protected function flush($positionPrefix = '') {
465
		// write to disk only when writeFlag was set to 1
466
		if ($this->writeFlag) {
467
			// Disable the file proxies so that encryption is not
468
			// automatically attempted when the file is written to disk -
469
			// we are handling that separately here and we don't want to
470
			// get into an infinite loop
471
			$position = (int)floor($this->position/$this->unencryptedBlockSize);
472
			$encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
473
			$bytesWritten = parent::stream_write($encrypted);
474
			$this->writeFlag = false;
475
			// Check whether the write concerns the last block
476
			// If so then update the encrypted filesize
477
			// Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
478
			// We recalculate the encrypted filesize as we do not know the context of calling flush()
479
			$completeBlocksInFile=(int)floor($this->unencryptedSize/$this->unencryptedBlockSize);
480
			if ($completeBlocksInFile === (int)floor($this->position/$this->unencryptedBlockSize)) {
481
				$this->size = $this->util->getBlockSize() * $completeBlocksInFile;
482
				$this->size += $bytesWritten;
483
				$this->size += $this->headerSize;
484
			}
485
		}
486
		// always empty the cache (otherwise readCache() will not fill it with the new block)
487
		$this->cache = '';
488
	}
489
490
	/**
491
	 * read block to file
492
	 */
493
	protected function readCache() {
494
		// cache should always be empty string when this function is called
495
		// don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
496
		if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
497
			// Get the data from the file handle
498
			$data = $this->stream_read_block($this->util->getBlockSize());
499
			$position = (int)floor($this->position/$this->unencryptedBlockSize);
500
			$numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
501
			if($numberOfChunks === $position) {
502
				$position .= 'end';
503
			}
504
			$this->cache = $this->encryptionModule->decrypt($data, $position);
505
		}
506
	}
507
508
	/**
509
	 * write header at beginning of encrypted file
510
	 *
511
	 * @return integer
512
	 * @throws EncryptionHeaderKeyExistsException if header key is already in use
513
	 */
514
	protected function writeHeader() {
515
		$header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
516
		return parent::stream_write($header);
517
	}
518
519
	/**
520
	 * read first block to skip the header
521
	 */
522
	protected function skipHeader() {
523
		$this->stream_read_block($this->headerSize);
524
	}
525
526
	/**
527
	 * call stream_seek() from parent class
528
	 *
529
	 * @param integer $position
530
	 * @return bool
531
	 */
532
	protected function parentStreamSeek($position) {
533
		return parent::stream_seek($position);
534
	}
535
536
	/**
537
	 * @param string $path
538
	 * @param array $options
539
	 * @return bool
540
	 */
541
	public function dir_opendir($path, $options) {
542
		return false;
543
	}
544
545
}
546