Passed
Push — master ( 001278...87e638 )
by Julius
14:47 queued 58s
created
lib/private/Files/Stream/Encryption.php 1 patch
Indentation   +519 added lines, -519 removed lines patch added patch discarded remove patch
@@ -38,523 +38,523 @@
 block discarded – undo
38 38
 
39 39
 class Encryption extends Wrapper {
40 40
 
41
-	/** @var \OC\Encryption\Util */
42
-	protected $util;
43
-
44
-	/** @var \OC\Encryption\File */
45
-	protected $file;
46
-
47
-	/** @var \OCP\Encryption\IEncryptionModule */
48
-	protected $encryptionModule;
49
-
50
-	/** @var \OC\Files\Storage\Storage */
51
-	protected $storage;
52
-
53
-	/** @var \OC\Files\Storage\Wrapper\Encryption */
54
-	protected $encryptionStorage;
55
-
56
-	/** @var string */
57
-	protected $internalPath;
58
-
59
-	/** @var string */
60
-	protected $cache;
61
-
62
-	/** @var integer */
63
-	protected $size;
64
-
65
-	/** @var integer */
66
-	protected $position;
67
-
68
-	/** @var integer */
69
-	protected $unencryptedSize;
70
-
71
-	/** @var integer */
72
-	protected $headerSize;
73
-
74
-	/** @var integer */
75
-	protected $unencryptedBlockSize;
76
-
77
-	/** @var array */
78
-	protected $header;
79
-
80
-	/** @var string */
81
-	protected $fullPath;
82
-
83
-	/** @var  bool */
84
-	protected $signed;
85
-
86
-	/**
87
-	 * header data returned by the encryption module, will be written to the file
88
-	 * in case of a write operation
89
-	 *
90
-	 * @var array
91
-	 */
92
-	protected $newHeader;
93
-
94
-	/**
95
-	 * user who perform the read/write operation null for public access
96
-	 *
97
-	 * @var string
98
-	 */
99
-	protected $uid;
100
-
101
-	/** @var bool */
102
-	protected $readOnly;
103
-
104
-	/** @var bool */
105
-	protected $writeFlag;
106
-
107
-	/** @var array */
108
-	protected $expectedContextProperties;
109
-
110
-	/** @var bool */
111
-	protected $fileUpdated;
112
-
113
-	public function __construct() {
114
-		$this->expectedContextProperties = [
115
-			'source',
116
-			'storage',
117
-			'internalPath',
118
-			'fullPath',
119
-			'encryptionModule',
120
-			'header',
121
-			'uid',
122
-			'file',
123
-			'util',
124
-			'size',
125
-			'unencryptedSize',
126
-			'encryptionStorage',
127
-			'headerSize',
128
-			'signed'
129
-		];
130
-	}
131
-
132
-
133
-	/**
134
-	 * Wraps a stream with the provided callbacks
135
-	 *
136
-	 * @param resource $source
137
-	 * @param string $internalPath relative to mount point
138
-	 * @param string $fullPath relative to data/
139
-	 * @param array $header
140
-	 * @param string $uid
141
-	 * @param \OCP\Encryption\IEncryptionModule $encryptionModule
142
-	 * @param \OC\Files\Storage\Storage $storage
143
-	 * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
144
-	 * @param \OC\Encryption\Util $util
145
-	 * @param \OC\Encryption\File $file
146
-	 * @param string $mode
147
-	 * @param int $size
148
-	 * @param int $unencryptedSize
149
-	 * @param int $headerSize
150
-	 * @param bool $signed
151
-	 * @param string $wrapper stream wrapper class
152
-	 * @return resource
153
-	 *
154
-	 * @throws \BadMethodCallException
155
-	 */
156
-	public static function wrap($source, $internalPath, $fullPath, array $header,
157
-								$uid,
158
-								\OCP\Encryption\IEncryptionModule $encryptionModule,
159
-								\OC\Files\Storage\Storage $storage,
160
-								\OC\Files\Storage\Wrapper\Encryption $encStorage,
161
-								\OC\Encryption\Util $util,
162
-								 \OC\Encryption\File $file,
163
-								$mode,
164
-								$size,
165
-								$unencryptedSize,
166
-								$headerSize,
167
-								$signed,
168
-								$wrapper = Encryption::class) {
169
-		$context = stream_context_create([
170
-			'ocencryption' => [
171
-				'source' => $source,
172
-				'storage' => $storage,
173
-				'internalPath' => $internalPath,
174
-				'fullPath' => $fullPath,
175
-				'encryptionModule' => $encryptionModule,
176
-				'header' => $header,
177
-				'uid' => $uid,
178
-				'util' => $util,
179
-				'file' => $file,
180
-				'size' => $size,
181
-				'unencryptedSize' => $unencryptedSize,
182
-				'encryptionStorage' => $encStorage,
183
-				'headerSize' => $headerSize,
184
-				'signed' => $signed
185
-			]
186
-		]);
187
-
188
-		return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
189
-	}
190
-
191
-	/**
192
-	 * add stream wrapper
193
-	 *
194
-	 * @param resource|int $source
195
-	 * @param resource|array $context
196
-	 * @param string|null $protocol
197
-	 * @param string|null $class
198
-	 * @param string $mode
199
-	 * @return resource
200
-	 * @throws \BadMethodCallException
201
-	 */
202
-	protected static function wrapSource($source, $context = [], $protocol = null, $class = null, $mode = 'r+') {
203
-		try {
204
-			if ($protocol === null) {
205
-				$protocol = self::getProtocol($class);
206
-			}
207
-
208
-			stream_wrapper_register($protocol, $class);
209
-			$context = self::buildContext($protocol, $context, $source);
210
-			if (self::isDirectoryHandle($source)) {
211
-				$wrapped = opendir($protocol . '://', $context);
212
-			} else {
213
-				$wrapped = fopen($protocol . '://', $mode, false, $context);
214
-			}
215
-		} catch (\Exception $e) {
216
-			stream_wrapper_unregister($protocol);
217
-			throw $e;
218
-		}
219
-		stream_wrapper_unregister($protocol);
220
-		return $wrapped;
221
-	}
222
-
223
-	/**
224
-	 * @todo this is a copy of \Icewind\Streams\WrapperHandler::buildContext -> combine to one shared method?
225
-	 */
226
-	private static function buildContext($protocol, $context, $source) {
227
-		if (is_array($context)) {
228
-			$context['source'] = $source;
229
-			return stream_context_create([$protocol => $context]);
230
-		}
231
-
232
-		return $context;
233
-	}
234
-
235
-	/**
236
-	 * Load the source from the stream context and return the context options
237
-	 *
238
-	 * @param string|null $name
239
-	 * @return array
240
-	 * @throws \BadMethodCallException
241
-	 */
242
-	protected function loadContext($name = null) {
243
-		$context = parent::loadContext($name);
244
-
245
-		foreach ($this->expectedContextProperties as $property) {
246
-			if (array_key_exists($property, $context)) {
247
-				$this->{$property} = $context[$property];
248
-			} else {
249
-				throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
250
-			}
251
-		}
252
-		return $context;
253
-	}
254
-
255
-	public function stream_open($path, $mode, $options, &$opened_path) {
256
-		$this->loadContext('ocencryption');
257
-
258
-		$this->position = 0;
259
-		$this->cache = '';
260
-		$this->writeFlag = false;
261
-		$this->fileUpdated = false;
262
-
263
-		if (
264
-			$mode === 'w'
265
-			|| $mode === 'w+'
266
-			|| $mode === 'wb'
267
-			|| $mode === 'wb+'
268
-			|| $mode === 'r+'
269
-			|| $mode === 'rb+'
270
-		) {
271
-			$this->readOnly = false;
272
-		} else {
273
-			$this->readOnly = true;
274
-		}
275
-
276
-		$sharePath = $this->fullPath;
277
-		if (!$this->storage->file_exists($this->internalPath)) {
278
-			$sharePath = dirname($sharePath);
279
-		}
280
-
281
-		$accessList = [];
282
-		if ($this->encryptionModule->needDetailedAccessList()) {
283
-			$accessList = $this->file->getAccessList($sharePath);
284
-		}
285
-		$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
286
-		$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
287
-
288
-		if (
289
-			$mode === 'w'
290
-			|| $mode === 'w+'
291
-			|| $mode === 'wb'
292
-			|| $mode === 'wb+'
293
-		) {
294
-			// We're writing a new file so start write counter with 0 bytes
295
-			$this->unencryptedSize = 0;
296
-			$this->writeHeader();
297
-			$this->headerSize = $this->util->getHeaderSize();
298
-			$this->size = $this->headerSize;
299
-		} else {
300
-			$this->skipHeader();
301
-		}
302
-
303
-		return true;
304
-	}
305
-
306
-	public function stream_eof() {
307
-		return $this->position >= $this->unencryptedSize;
308
-	}
309
-
310
-	public function stream_read($count) {
311
-		$result = '';
312
-
313
-		$count = min($count, $this->unencryptedSize - $this->position);
314
-		while ($count > 0) {
315
-			$remainingLength = $count;
316
-			// update the cache of the current block
317
-			$this->readCache();
318
-			// determine the relative position in the current block
319
-			$blockPosition = ($this->position % $this->unencryptedBlockSize);
320
-			// if entire read inside current block then only position needs to be updated
321
-			if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
322
-				$result .= substr($this->cache, $blockPosition, $remainingLength);
323
-				$this->position += $remainingLength;
324
-				$count = 0;
325
-			// otherwise remainder of current block is fetched, the block is flushed and the position updated
326
-			} else {
327
-				$result .= substr($this->cache, $blockPosition);
328
-				$this->flush();
329
-				$this->position += ($this->unencryptedBlockSize - $blockPosition);
330
-				$count -= ($this->unencryptedBlockSize - $blockPosition);
331
-			}
332
-		}
333
-		return $result;
334
-	}
335
-
336
-	/**
337
-	 * stream_read_block
338
-	 *
339
-	 * This function is a wrapper for function stream_read.
340
-	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
341
-	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
342
-	 * remote storage over the internet and it does not care about the given $blockSize.
343
-	 *
344
-	 * @param int $blockSize Length of requested data block in bytes
345
-	 * @return string Data fetched from stream.
346
-	 */
347
-	private function stream_read_block(int $blockSize): string {
348
-		$remaining = $blockSize;
349
-		$data = '';
350
-
351
-		do {
352
-			$chunk = parent::stream_read($remaining);
353
-			$chunk_len = strlen($chunk);
354
-			$data .= $chunk;
355
-			$remaining -= $chunk_len;
356
-		} while (($remaining > 0) && ($chunk_len > 0));
357
-
358
-		return $data;
359
-	}
360
-
361
-	public function stream_write($data) {
362
-		$length = 0;
363
-		// loop over $data to fit it in 6126 sized unencrypted blocks
364
-		while (isset($data[0])) {
365
-			$remainingLength = strlen($data);
366
-
367
-			// set the cache to the current 6126 block
368
-			$this->readCache();
369
-
370
-			// for seekable streams the pointer is moved back to the beginning of the encrypted block
371
-			// flush will start writing there when the position moves to another block
372
-			$positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
373
-				$this->util->getBlockSize() + $this->headerSize;
374
-			$resultFseek = $this->parentStreamSeek($positionInFile);
375
-
376
-			// only allow writes on seekable streams, or at the end of the encrypted stream
377
-			if (!$this->readOnly && ($resultFseek || $positionInFile === $this->size)) {
378
-
379
-				// switch the writeFlag so flush() will write the block
380
-				$this->writeFlag = true;
381
-				$this->fileUpdated = true;
382
-
383
-				// determine the relative position in the current block
384
-				$blockPosition = ($this->position % $this->unencryptedBlockSize);
385
-				// check if $data fits in current block
386
-				// if so, overwrite existing data (if any)
387
-				// update position and liberate $data
388
-				if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
389
-					$this->cache = substr($this->cache, 0, $blockPosition)
390
-						. $data . substr($this->cache, $blockPosition + $remainingLength);
391
-					$this->position += $remainingLength;
392
-					$length += $remainingLength;
393
-					$data = '';
394
-				// if $data doesn't fit the current block, the fill the current block and reiterate
395
-					// after the block is filled, it is flushed and $data is updatedxxx
396
-				} else {
397
-					$this->cache = substr($this->cache, 0, $blockPosition) .
398
-						substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
399
-					$this->flush();
400
-					$this->position += ($this->unencryptedBlockSize - $blockPosition);
401
-					$length += ($this->unencryptedBlockSize - $blockPosition);
402
-					$data = substr($data, $this->unencryptedBlockSize - $blockPosition);
403
-				}
404
-			} else {
405
-				$data = '';
406
-			}
407
-			$this->unencryptedSize = max($this->unencryptedSize, $this->position);
408
-		}
409
-		return $length;
410
-	}
411
-
412
-	public function stream_tell() {
413
-		return $this->position;
414
-	}
415
-
416
-	public function stream_seek($offset, $whence = SEEK_SET) {
417
-		$return = false;
418
-
419
-		switch ($whence) {
420
-			case SEEK_SET:
421
-				$newPosition = $offset;
422
-				break;
423
-			case SEEK_CUR:
424
-				$newPosition = $this->position + $offset;
425
-				break;
426
-			case SEEK_END:
427
-				$newPosition = $this->unencryptedSize + $offset;
428
-				break;
429
-			default:
430
-				return $return;
431
-		}
432
-
433
-		if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
434
-			return $return;
435
-		}
436
-
437
-		$newFilePosition = (int)floor($newPosition / $this->unencryptedBlockSize)
438
-			* $this->util->getBlockSize() + $this->headerSize;
439
-
440
-		$oldFilePosition = parent::stream_tell();
441
-		if ($this->parentStreamSeek($newFilePosition)) {
442
-			$this->parentStreamSeek($oldFilePosition);
443
-			$this->flush();
444
-			$this->parentStreamSeek($newFilePosition);
445
-			$this->position = $newPosition;
446
-			$return = true;
447
-		}
448
-		return $return;
449
-	}
450
-
451
-	public function stream_close() {
452
-		$this->flush('end');
453
-		$position = (int)floor($this->position / $this->unencryptedBlockSize);
454
-		$remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
455
-		if ($this->readOnly === false) {
456
-			if (!empty($remainingData)) {
457
-				parent::stream_write($remainingData);
458
-			}
459
-			$this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
460
-		}
461
-		$result = parent::stream_close();
462
-
463
-		if ($this->fileUpdated) {
464
-			$cache = $this->storage->getCache();
465
-			$cacheEntry = $cache->get($this->internalPath);
466
-			if ($cacheEntry) {
467
-				$version = $cacheEntry['encryptedVersion'] + 1;
468
-				$cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize]);
469
-			}
470
-		}
471
-
472
-		return $result;
473
-	}
474
-
475
-	/**
476
-	 * write block to file
477
-	 * @param string $positionPrefix
478
-	 */
479
-	protected function flush($positionPrefix = '') {
480
-		// write to disk only when writeFlag was set to 1
481
-		if ($this->writeFlag) {
482
-			// Disable the file proxies so that encryption is not
483
-			// automatically attempted when the file is written to disk -
484
-			// we are handling that separately here and we don't want to
485
-			// get into an infinite loop
486
-			$position = (int)floor($this->position / $this->unencryptedBlockSize);
487
-			$encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
488
-			$bytesWritten = parent::stream_write($encrypted);
489
-			$this->writeFlag = false;
490
-			// Check whether the write concerns the last block
491
-			// If so then update the encrypted filesize
492
-			// Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
493
-			// We recalculate the encrypted filesize as we do not know the context of calling flush()
494
-			$completeBlocksInFile = (int)floor($this->unencryptedSize / $this->unencryptedBlockSize);
495
-			if ($completeBlocksInFile === (int)floor($this->position / $this->unencryptedBlockSize)) {
496
-				$this->size = $this->util->getBlockSize() * $completeBlocksInFile;
497
-				$this->size += $bytesWritten;
498
-				$this->size += $this->headerSize;
499
-			}
500
-		}
501
-		// always empty the cache (otherwise readCache() will not fill it with the new block)
502
-		$this->cache = '';
503
-	}
504
-
505
-	/**
506
-	 * read block to file
507
-	 */
508
-	protected function readCache() {
509
-		// cache should always be empty string when this function is called
510
-		// don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
511
-		if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
512
-			// Get the data from the file handle
513
-			$data = $this->stream_read_block($this->util->getBlockSize());
514
-			$position = (int)floor($this->position / $this->unencryptedBlockSize);
515
-			$numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
516
-			if ($numberOfChunks === $position) {
517
-				$position .= 'end';
518
-			}
519
-			$this->cache = $this->encryptionModule->decrypt($data, $position);
520
-		}
521
-	}
522
-
523
-	/**
524
-	 * write header at beginning of encrypted file
525
-	 *
526
-	 * @return int|false
527
-	 * @throws EncryptionHeaderKeyExistsException if header key is already in use
528
-	 */
529
-	protected function writeHeader() {
530
-		$header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
531
-		$this->fileUpdated = true;
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
-	}
41
+    /** @var \OC\Encryption\Util */
42
+    protected $util;
43
+
44
+    /** @var \OC\Encryption\File */
45
+    protected $file;
46
+
47
+    /** @var \OCP\Encryption\IEncryptionModule */
48
+    protected $encryptionModule;
49
+
50
+    /** @var \OC\Files\Storage\Storage */
51
+    protected $storage;
52
+
53
+    /** @var \OC\Files\Storage\Wrapper\Encryption */
54
+    protected $encryptionStorage;
55
+
56
+    /** @var string */
57
+    protected $internalPath;
58
+
59
+    /** @var string */
60
+    protected $cache;
61
+
62
+    /** @var integer */
63
+    protected $size;
64
+
65
+    /** @var integer */
66
+    protected $position;
67
+
68
+    /** @var integer */
69
+    protected $unencryptedSize;
70
+
71
+    /** @var integer */
72
+    protected $headerSize;
73
+
74
+    /** @var integer */
75
+    protected $unencryptedBlockSize;
76
+
77
+    /** @var array */
78
+    protected $header;
79
+
80
+    /** @var string */
81
+    protected $fullPath;
82
+
83
+    /** @var  bool */
84
+    protected $signed;
85
+
86
+    /**
87
+     * header data returned by the encryption module, will be written to the file
88
+     * in case of a write operation
89
+     *
90
+     * @var array
91
+     */
92
+    protected $newHeader;
93
+
94
+    /**
95
+     * user who perform the read/write operation null for public access
96
+     *
97
+     * @var string
98
+     */
99
+    protected $uid;
100
+
101
+    /** @var bool */
102
+    protected $readOnly;
103
+
104
+    /** @var bool */
105
+    protected $writeFlag;
106
+
107
+    /** @var array */
108
+    protected $expectedContextProperties;
109
+
110
+    /** @var bool */
111
+    protected $fileUpdated;
112
+
113
+    public function __construct() {
114
+        $this->expectedContextProperties = [
115
+            'source',
116
+            'storage',
117
+            'internalPath',
118
+            'fullPath',
119
+            'encryptionModule',
120
+            'header',
121
+            'uid',
122
+            'file',
123
+            'util',
124
+            'size',
125
+            'unencryptedSize',
126
+            'encryptionStorage',
127
+            'headerSize',
128
+            'signed'
129
+        ];
130
+    }
131
+
132
+
133
+    /**
134
+     * Wraps a stream with the provided callbacks
135
+     *
136
+     * @param resource $source
137
+     * @param string $internalPath relative to mount point
138
+     * @param string $fullPath relative to data/
139
+     * @param array $header
140
+     * @param string $uid
141
+     * @param \OCP\Encryption\IEncryptionModule $encryptionModule
142
+     * @param \OC\Files\Storage\Storage $storage
143
+     * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
144
+     * @param \OC\Encryption\Util $util
145
+     * @param \OC\Encryption\File $file
146
+     * @param string $mode
147
+     * @param int $size
148
+     * @param int $unencryptedSize
149
+     * @param int $headerSize
150
+     * @param bool $signed
151
+     * @param string $wrapper stream wrapper class
152
+     * @return resource
153
+     *
154
+     * @throws \BadMethodCallException
155
+     */
156
+    public static function wrap($source, $internalPath, $fullPath, array $header,
157
+                                $uid,
158
+                                \OCP\Encryption\IEncryptionModule $encryptionModule,
159
+                                \OC\Files\Storage\Storage $storage,
160
+                                \OC\Files\Storage\Wrapper\Encryption $encStorage,
161
+                                \OC\Encryption\Util $util,
162
+                                    \OC\Encryption\File $file,
163
+                                $mode,
164
+                                $size,
165
+                                $unencryptedSize,
166
+                                $headerSize,
167
+                                $signed,
168
+                                $wrapper = Encryption::class) {
169
+        $context = stream_context_create([
170
+            'ocencryption' => [
171
+                'source' => $source,
172
+                'storage' => $storage,
173
+                'internalPath' => $internalPath,
174
+                'fullPath' => $fullPath,
175
+                'encryptionModule' => $encryptionModule,
176
+                'header' => $header,
177
+                'uid' => $uid,
178
+                'util' => $util,
179
+                'file' => $file,
180
+                'size' => $size,
181
+                'unencryptedSize' => $unencryptedSize,
182
+                'encryptionStorage' => $encStorage,
183
+                'headerSize' => $headerSize,
184
+                'signed' => $signed
185
+            ]
186
+        ]);
187
+
188
+        return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
189
+    }
190
+
191
+    /**
192
+     * add stream wrapper
193
+     *
194
+     * @param resource|int $source
195
+     * @param resource|array $context
196
+     * @param string|null $protocol
197
+     * @param string|null $class
198
+     * @param string $mode
199
+     * @return resource
200
+     * @throws \BadMethodCallException
201
+     */
202
+    protected static function wrapSource($source, $context = [], $protocol = null, $class = null, $mode = 'r+') {
203
+        try {
204
+            if ($protocol === null) {
205
+                $protocol = self::getProtocol($class);
206
+            }
207
+
208
+            stream_wrapper_register($protocol, $class);
209
+            $context = self::buildContext($protocol, $context, $source);
210
+            if (self::isDirectoryHandle($source)) {
211
+                $wrapped = opendir($protocol . '://', $context);
212
+            } else {
213
+                $wrapped = fopen($protocol . '://', $mode, false, $context);
214
+            }
215
+        } catch (\Exception $e) {
216
+            stream_wrapper_unregister($protocol);
217
+            throw $e;
218
+        }
219
+        stream_wrapper_unregister($protocol);
220
+        return $wrapped;
221
+    }
222
+
223
+    /**
224
+     * @todo this is a copy of \Icewind\Streams\WrapperHandler::buildContext -> combine to one shared method?
225
+     */
226
+    private static function buildContext($protocol, $context, $source) {
227
+        if (is_array($context)) {
228
+            $context['source'] = $source;
229
+            return stream_context_create([$protocol => $context]);
230
+        }
231
+
232
+        return $context;
233
+    }
234
+
235
+    /**
236
+     * Load the source from the stream context and return the context options
237
+     *
238
+     * @param string|null $name
239
+     * @return array
240
+     * @throws \BadMethodCallException
241
+     */
242
+    protected function loadContext($name = null) {
243
+        $context = parent::loadContext($name);
244
+
245
+        foreach ($this->expectedContextProperties as $property) {
246
+            if (array_key_exists($property, $context)) {
247
+                $this->{$property} = $context[$property];
248
+            } else {
249
+                throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
250
+            }
251
+        }
252
+        return $context;
253
+    }
254
+
255
+    public function stream_open($path, $mode, $options, &$opened_path) {
256
+        $this->loadContext('ocencryption');
257
+
258
+        $this->position = 0;
259
+        $this->cache = '';
260
+        $this->writeFlag = false;
261
+        $this->fileUpdated = false;
262
+
263
+        if (
264
+            $mode === 'w'
265
+            || $mode === 'w+'
266
+            || $mode === 'wb'
267
+            || $mode === 'wb+'
268
+            || $mode === 'r+'
269
+            || $mode === 'rb+'
270
+        ) {
271
+            $this->readOnly = false;
272
+        } else {
273
+            $this->readOnly = true;
274
+        }
275
+
276
+        $sharePath = $this->fullPath;
277
+        if (!$this->storage->file_exists($this->internalPath)) {
278
+            $sharePath = dirname($sharePath);
279
+        }
280
+
281
+        $accessList = [];
282
+        if ($this->encryptionModule->needDetailedAccessList()) {
283
+            $accessList = $this->file->getAccessList($sharePath);
284
+        }
285
+        $this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
286
+        $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
287
+
288
+        if (
289
+            $mode === 'w'
290
+            || $mode === 'w+'
291
+            || $mode === 'wb'
292
+            || $mode === 'wb+'
293
+        ) {
294
+            // We're writing a new file so start write counter with 0 bytes
295
+            $this->unencryptedSize = 0;
296
+            $this->writeHeader();
297
+            $this->headerSize = $this->util->getHeaderSize();
298
+            $this->size = $this->headerSize;
299
+        } else {
300
+            $this->skipHeader();
301
+        }
302
+
303
+        return true;
304
+    }
305
+
306
+    public function stream_eof() {
307
+        return $this->position >= $this->unencryptedSize;
308
+    }
309
+
310
+    public function stream_read($count) {
311
+        $result = '';
312
+
313
+        $count = min($count, $this->unencryptedSize - $this->position);
314
+        while ($count > 0) {
315
+            $remainingLength = $count;
316
+            // update the cache of the current block
317
+            $this->readCache();
318
+            // determine the relative position in the current block
319
+            $blockPosition = ($this->position % $this->unencryptedBlockSize);
320
+            // if entire read inside current block then only position needs to be updated
321
+            if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
322
+                $result .= substr($this->cache, $blockPosition, $remainingLength);
323
+                $this->position += $remainingLength;
324
+                $count = 0;
325
+            // otherwise remainder of current block is fetched, the block is flushed and the position updated
326
+            } else {
327
+                $result .= substr($this->cache, $blockPosition);
328
+                $this->flush();
329
+                $this->position += ($this->unencryptedBlockSize - $blockPosition);
330
+                $count -= ($this->unencryptedBlockSize - $blockPosition);
331
+            }
332
+        }
333
+        return $result;
334
+    }
335
+
336
+    /**
337
+     * stream_read_block
338
+     *
339
+     * This function is a wrapper for function stream_read.
340
+     * It calls stream read until the requested $blockSize was received or no remaining data is present.
341
+     * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
342
+     * remote storage over the internet and it does not care about the given $blockSize.
343
+     *
344
+     * @param int $blockSize Length of requested data block in bytes
345
+     * @return string Data fetched from stream.
346
+     */
347
+    private function stream_read_block(int $blockSize): string {
348
+        $remaining = $blockSize;
349
+        $data = '';
350
+
351
+        do {
352
+            $chunk = parent::stream_read($remaining);
353
+            $chunk_len = strlen($chunk);
354
+            $data .= $chunk;
355
+            $remaining -= $chunk_len;
356
+        } while (($remaining > 0) && ($chunk_len > 0));
357
+
358
+        return $data;
359
+    }
360
+
361
+    public function stream_write($data) {
362
+        $length = 0;
363
+        // loop over $data to fit it in 6126 sized unencrypted blocks
364
+        while (isset($data[0])) {
365
+            $remainingLength = strlen($data);
366
+
367
+            // set the cache to the current 6126 block
368
+            $this->readCache();
369
+
370
+            // for seekable streams the pointer is moved back to the beginning of the encrypted block
371
+            // flush will start writing there when the position moves to another block
372
+            $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
373
+                $this->util->getBlockSize() + $this->headerSize;
374
+            $resultFseek = $this->parentStreamSeek($positionInFile);
375
+
376
+            // only allow writes on seekable streams, or at the end of the encrypted stream
377
+            if (!$this->readOnly && ($resultFseek || $positionInFile === $this->size)) {
378
+
379
+                // switch the writeFlag so flush() will write the block
380
+                $this->writeFlag = true;
381
+                $this->fileUpdated = true;
382
+
383
+                // determine the relative position in the current block
384
+                $blockPosition = ($this->position % $this->unencryptedBlockSize);
385
+                // check if $data fits in current block
386
+                // if so, overwrite existing data (if any)
387
+                // update position and liberate $data
388
+                if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
389
+                    $this->cache = substr($this->cache, 0, $blockPosition)
390
+                        . $data . substr($this->cache, $blockPosition + $remainingLength);
391
+                    $this->position += $remainingLength;
392
+                    $length += $remainingLength;
393
+                    $data = '';
394
+                // if $data doesn't fit the current block, the fill the current block and reiterate
395
+                    // after the block is filled, it is flushed and $data is updatedxxx
396
+                } else {
397
+                    $this->cache = substr($this->cache, 0, $blockPosition) .
398
+                        substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
399
+                    $this->flush();
400
+                    $this->position += ($this->unencryptedBlockSize - $blockPosition);
401
+                    $length += ($this->unencryptedBlockSize - $blockPosition);
402
+                    $data = substr($data, $this->unencryptedBlockSize - $blockPosition);
403
+                }
404
+            } else {
405
+                $data = '';
406
+            }
407
+            $this->unencryptedSize = max($this->unencryptedSize, $this->position);
408
+        }
409
+        return $length;
410
+    }
411
+
412
+    public function stream_tell() {
413
+        return $this->position;
414
+    }
415
+
416
+    public function stream_seek($offset, $whence = SEEK_SET) {
417
+        $return = false;
418
+
419
+        switch ($whence) {
420
+            case SEEK_SET:
421
+                $newPosition = $offset;
422
+                break;
423
+            case SEEK_CUR:
424
+                $newPosition = $this->position + $offset;
425
+                break;
426
+            case SEEK_END:
427
+                $newPosition = $this->unencryptedSize + $offset;
428
+                break;
429
+            default:
430
+                return $return;
431
+        }
432
+
433
+        if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
434
+            return $return;
435
+        }
436
+
437
+        $newFilePosition = (int)floor($newPosition / $this->unencryptedBlockSize)
438
+            * $this->util->getBlockSize() + $this->headerSize;
439
+
440
+        $oldFilePosition = parent::stream_tell();
441
+        if ($this->parentStreamSeek($newFilePosition)) {
442
+            $this->parentStreamSeek($oldFilePosition);
443
+            $this->flush();
444
+            $this->parentStreamSeek($newFilePosition);
445
+            $this->position = $newPosition;
446
+            $return = true;
447
+        }
448
+        return $return;
449
+    }
450
+
451
+    public function stream_close() {
452
+        $this->flush('end');
453
+        $position = (int)floor($this->position / $this->unencryptedBlockSize);
454
+        $remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
455
+        if ($this->readOnly === false) {
456
+            if (!empty($remainingData)) {
457
+                parent::stream_write($remainingData);
458
+            }
459
+            $this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
460
+        }
461
+        $result = parent::stream_close();
462
+
463
+        if ($this->fileUpdated) {
464
+            $cache = $this->storage->getCache();
465
+            $cacheEntry = $cache->get($this->internalPath);
466
+            if ($cacheEntry) {
467
+                $version = $cacheEntry['encryptedVersion'] + 1;
468
+                $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize]);
469
+            }
470
+        }
471
+
472
+        return $result;
473
+    }
474
+
475
+    /**
476
+     * write block to file
477
+     * @param string $positionPrefix
478
+     */
479
+    protected function flush($positionPrefix = '') {
480
+        // write to disk only when writeFlag was set to 1
481
+        if ($this->writeFlag) {
482
+            // Disable the file proxies so that encryption is not
483
+            // automatically attempted when the file is written to disk -
484
+            // we are handling that separately here and we don't want to
485
+            // get into an infinite loop
486
+            $position = (int)floor($this->position / $this->unencryptedBlockSize);
487
+            $encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
488
+            $bytesWritten = parent::stream_write($encrypted);
489
+            $this->writeFlag = false;
490
+            // Check whether the write concerns the last block
491
+            // If so then update the encrypted filesize
492
+            // Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
493
+            // We recalculate the encrypted filesize as we do not know the context of calling flush()
494
+            $completeBlocksInFile = (int)floor($this->unencryptedSize / $this->unencryptedBlockSize);
495
+            if ($completeBlocksInFile === (int)floor($this->position / $this->unencryptedBlockSize)) {
496
+                $this->size = $this->util->getBlockSize() * $completeBlocksInFile;
497
+                $this->size += $bytesWritten;
498
+                $this->size += $this->headerSize;
499
+            }
500
+        }
501
+        // always empty the cache (otherwise readCache() will not fill it with the new block)
502
+        $this->cache = '';
503
+    }
504
+
505
+    /**
506
+     * read block to file
507
+     */
508
+    protected function readCache() {
509
+        // cache should always be empty string when this function is called
510
+        // don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
511
+        if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
512
+            // Get the data from the file handle
513
+            $data = $this->stream_read_block($this->util->getBlockSize());
514
+            $position = (int)floor($this->position / $this->unencryptedBlockSize);
515
+            $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
516
+            if ($numberOfChunks === $position) {
517
+                $position .= 'end';
518
+            }
519
+            $this->cache = $this->encryptionModule->decrypt($data, $position);
520
+        }
521
+    }
522
+
523
+    /**
524
+     * write header at beginning of encrypted file
525
+     *
526
+     * @return int|false
527
+     * @throws EncryptionHeaderKeyExistsException if header key is already in use
528
+     */
529
+    protected function writeHeader() {
530
+        $header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
531
+        $this->fileUpdated = true;
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 560
 }
Please login to merge, or discard this patch.