encryption.php 12.1 KB
Newer Older
1
2
<?php
/**
3
4
5
 * @author Björn Schießle <schiessle@owncloud.com>
 * @author jknockaert <jasper@knockaert.nl>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
6
 *
7
8
 * @copyright Copyright (c) 2015, ownCloud, Inc.
 * @license AGPL-3.0
9
 *
10
11
12
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
17
18
19
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
22
23
24
25
26
27
28
29
30
31
32
33
 *
 */

namespace OC\Files\Stream;

use Icewind\Streams\Wrapper;
use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;

class Encryption extends Wrapper {

	/** @var \OC\Encryption\Util */
	protected $util;

34
35
36
	/** @var \OC\Encryption\File */
	protected $file;

37
38
39
40
41
42
43
44
45
46
47
48
	/** @var \OCP\Encryption\IEncryptionModule */
	protected $encryptionModule;

	/** @var \OC\Files\Storage\Storage */
	protected $storage;

	/** @var \OC\Files\Storage\Wrapper\Encryption */
	protected $encryptionStorage;

	/** @var string */
	protected $internalPath;

49
50
51
	/** @var string */
	protected $cache;

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
	/** @var integer */
	protected $size;

	/** @var integer */
	protected $position;

	/** @var integer */
	protected $unencryptedSize;

	/** @var integer */
	protected $unencryptedBlockSize;

	/** @var array */
	protected $header;

	/** @var string */
	protected $fullPath;

	/**
	 * header data returned by the encryption module, will be written to the file
	 * in case of a write operation
	 *
	 * @var array
	 */
	protected $newHeader;

	/**
	 * user who perform the read/write operation null for public access
	 *
81
	 * @var string
82
83
84
85
86
87
	 */
	protected $uid;

	/** @var bool */
	protected $readOnly;

88
89
90
	/** @var bool */
	protected $writeFlag;

91
92
93
94
95
96
97
98
99
100
101
102
	/** @var array */
	protected $expectedContextProperties;

	public function __construct() {
		$this->expectedContextProperties = array(
			'source',
			'storage',
			'internalPath',
			'fullPath',
			'encryptionModule',
			'header',
			'uid',
103
			'file',
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
			'util',
			'size',
			'unencryptedSize',
			'encryptionStorage'
		);
	}


	/**
	 * Wraps a stream with the provided callbacks
	 *
	 * @param resource $source
	 * @param string $internalPath relative to mount point
	 * @param string $fullPath relative to data/
	 * @param array $header
119
	 * @param string $uid
120
121
	 * @param \OCP\Encryption\IEncryptionModule $encryptionModule
	 * @param \OC\Files\Storage\Storage $storage
122
	 * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
123
	 * @param \OC\Encryption\Util $util
124
	 * @param \OC\Encryption\File $file
125
126
127
128
129
130
131
132
	 * @param string $mode
	 * @param int $size
	 * @param int $unencryptedSize
	 * @return resource
	 *
	 * @throws \BadMethodCallException
	 */
	public static function wrap($source, $internalPath, $fullPath, array $header,
133
134
135
136
137
								$uid,
								\OCP\Encryption\IEncryptionModule $encryptionModule,
								\OC\Files\Storage\Storage $storage,
								\OC\Files\Storage\Wrapper\Encryption $encStorage,
								\OC\Encryption\Util $util,
138
								 \OC\Encryption\File $file,
139
140
141
								$mode,
								$size,
								$unencryptedSize) {
142
143
144
145
146
147
148
149
150
151
152

		$context = stream_context_create(array(
			'ocencryption' => array(
				'source' => $source,
				'storage' => $storage,
				'internalPath' => $internalPath,
				'fullPath' => $fullPath,
				'encryptionModule' => $encryptionModule,
				'header' => $header,
				'uid' => $uid,
				'util' => $util,
153
				'file' => $file,
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
				'size' => $size,
				'unencryptedSize' => $unencryptedSize,
				'encryptionStorage' => $encStorage
			)
		));

		return self::wrapSource($source, $mode, $context, 'ocencryption', 'OC\Files\Stream\Encryption');
	}

	/**
	 * add stream wrapper
	 *
	 * @param resource $source
	 * @param string $mode
	 * @param array $context
	 * @param string $protocol
	 * @param string $class
	 * @return resource
	 * @throws \BadMethodCallException
	 */
	protected static function wrapSource($source, $mode, $context, $protocol, $class) {
		try {
			stream_wrapper_register($protocol, $class);
			if (@rewinddir($source) === false) {
				$wrapped = fopen($protocol . '://', $mode, false, $context);
			} else {
				$wrapped = opendir($protocol . '://', $context);
			}
		} catch (\BadMethodCallException $e) {
			stream_wrapper_unregister($protocol);
			throw $e;
		}
		stream_wrapper_unregister($protocol);
		return $wrapped;
	}

	/**
	 * Load the source from the stream context and return the context options
	 *
	 * @param string $name
	 * @return array
	 * @throws \BadMethodCallException
	 */
	protected function loadContext($name) {
		$context = parent::loadContext($name);

		foreach ($this->expectedContextProperties as $property) {
201
			if (array_key_exists($property, $context)) {
202
203
204
205
206
207
208
209
210
211
212
213
214
				$this->{$property} = $context[$property];
			} else {
				throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
			}
		}
		return $context;

	}

	public function stream_open($path, $mode, $options, &$opened_path) {
		$this->loadContext('ocencryption');

		$this->position = 0;
215
216
		$this->cache = '';
		$this->writeFlag = false;
217
218
219
220
221
222
223
		$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize();

		if (
			$mode === 'w'
			|| $mode === 'w+'
			|| $mode === 'wb'
			|| $mode === 'wb+'
jknockaert's avatar
jknockaert committed
224
225
			|| $mode === 'r+'
			|| $mode === 'rb+'
226
227
228
229
230
231
		) {
			$this->readOnly = false;
		} else {
			$this->readOnly = true;
		}

jknockaert's avatar
jknockaert committed
232
233
234
235
236
237
238
239
		$sharePath = $this->fullPath;
		if (!$this->storage->file_exists($this->internalPath)) {
			$sharePath = dirname($sharePath);
		}

		$accessList = $this->file->getAccessList($sharePath);
		$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $this->header, $accessList);

jknockaert's avatar
jknockaert committed
240
241
242
243
244
245
246
247
248
249
250
251
252
253
		if (
			$mode === 'w'
			|| $mode === 'w+'
			|| $mode === 'wb'
			|| $mode === 'wb+'
		) {
			// We're writing a new file so start write counter with 0 bytes
			$this->unencryptedSize = 0;
			$this->writeHeader();
			$this->size = $this->util->getHeaderSize();
		} else {
			parent::stream_read($this->util->getHeaderSize());
		}

254
255
256
257
		return true;

	}

258
259
260
261
	public function stream_eof() {
		return $this->position >= $this->unencryptedSize;
	}

262
263
264
265
	public function stream_read($count) {

		$result = '';

266
//		$count = min($count, $this->unencryptedSize - $this->position);
267
268
269
		while ($count > 0) {
			$remainingLength = $count;
			// update the cache of the current block
270
			$this->readCache();
271
272
273
274
			// determine the relative position in the current block
			$blockPosition = ($this->position % $this->unencryptedBlockSize);
			// if entire read inside current block then only position needs to be updated
			if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
275
				$result .= substr($this->cache, $blockPosition, $remainingLength);
276
277
				$this->position += $remainingLength;
				$count = 0;
278
				// otherwise remainder of current block is fetched, the block is flushed and the position updated
279
			} else {
280
281
				$result .= substr($this->cache, $blockPosition);
				$this->flush();
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
				$this->position += ($this->unencryptedBlockSize - $blockPosition);
				$count -= ($this->unencryptedBlockSize - $blockPosition);
			}
		}
		return $result;

	}

	public function stream_write($data) {

		$length = 0;
		// loop over $data to fit it in 6126 sized unencrypted blocks
		while (strlen($data) > 0) {
			$remainingLength = strlen($data);

297
298
			// set the cache to the current 6126 block
			$this->readCache();
299
300
301

			// for seekable streams the pointer is moved back to the beginning of the encrypted block
			// flush will start writing there when the position moves to another block
302
			$positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
303
304
305
306
				$this->util->getBlockSize() + $this->util->getHeaderSize();
			$resultFseek = parent::stream_seek($positionInFile);

			// only allow writes on seekable streams, or at the end of the encrypted stream
307
308
309
			if (!($this->readOnly) && ($resultFseek || $positionInFile === $this->size)) {

				// switch the writeFlag so flush() will write the block
310
				$this->writeFlag = true;
311
312
313
314
315
316
317

				// determine the relative position in the current block
				$blockPosition = ($this->position % $this->unencryptedBlockSize);
				// check if $data fits in current block
				// if so, overwrite existing data (if any)
				// update position and liberate $data
				if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
318
319
					$this->cache = substr($this->cache, 0, $blockPosition)
						. $data . substr($this->cache, $blockPosition + $remainingLength);
320
321
322
					$this->position += $remainingLength;
					$length += $remainingLength;
					$data = '';
323
324
					// if $data doesn't fit the current block, the fill the current block and reiterate
					// after the block is filled, it is flushed and $data is updatedxxx
325
				} else {
326
					$this->cache = substr($this->cache, 0, $blockPosition) .
327
						substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
328
					$this->flush();
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
					$this->position += ($this->unencryptedBlockSize - $blockPosition);
					$length += ($this->unencryptedBlockSize - $blockPosition);
					$data = substr($data, $this->unencryptedBlockSize - $blockPosition);
				}
			} else {
				$data = '';
			}
		}
		$this->unencryptedSize = max($this->unencryptedSize, $this->position);
		return $length;
	}

	public function stream_tell() {
		return $this->position;
	}

	public function stream_seek($offset, $whence = SEEK_SET) {

		$return = false;

		switch ($whence) {
			case SEEK_SET:
				if ($offset < $this->unencryptedSize && $offset >= 0) {
					$newPosition = $offset;
				}
				break;
			case SEEK_CUR:
				if ($offset >= 0) {
					$newPosition = $offset + $this->position;
				}
				break;
			case SEEK_END:
				if ($this->unencryptedSize + $offset >= 0) {
					$newPosition = $this->unencryptedSize + $offset;
				}
				break;
			default:
				return $return;
		}

		$newFilePosition = floor($newPosition / $this->unencryptedBlockSize)
			* $this->util->getBlockSize() + $this->util->getHeaderSize();

372
		$oldFilePosition = parent::stream_tell();
373
		if (parent::stream_seek($newFilePosition)) {
374
			parent::stream_seek($oldFilePosition);
375
			$this->flush();
376
			parent::stream_seek($newFilePosition);
377
378
379
380
381
382
383
384
385
			$this->position = $newPosition;
			$return = true;
		}
		return $return;

	}

	public function stream_close() {
		$this->flush();
386
387
388
389
390
391
392
		$remainingData = $this->encryptionModule->end($this->fullPath);
		if ($this->readOnly === false) {
			if(!empty($remainingData)) {
				parent::stream_write($remainingData);
			}
			$this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
		}
393
394
395
396
		return parent::stream_close();
	}

	/**
397
	 * write block to file
398
399
	 */
	protected function flush() {
400
401
402
403
404
405
406
407
408
		// write to disk only when writeFlag was set to 1
		if ($this->writeFlag) {
			// Disable the file proxies so that encryption is not
			// automatically attempted when the file is written to disk -
			// we are handling that separately here and we don't want to
			// get into an infinite loop
			$encrypted = $this->encryptionModule->encrypt($this->cache);
			parent::stream_write($encrypted);
			$this->writeFlag = false;
409
			$this->size = max($this->size, parent::stream_tell());
410
		}
411
412
		// always empty the cache (otherwise readCache() will not fill it with the new block)
		$this->cache = '';
413
414
	}

415
416
417
418
419
420
	/**
	 * read block to file
	 */
	protected function readCache() {
		// cache should always be empty string when this function is called
		// don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
421
		if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
422
423
424
425
426
			// Get the data from the file handle
			$data = parent::stream_read($this->util->getBlockSize());
			$this->cache = $this->encryptionModule->decrypt($data);
		}
	}
427
428
429
430

	/**
	 * write header at beginning of encrypted file
	 *
431
	 * @return integer
432
433
434
435
	 * @throws EncryptionHeaderKeyExistsException if header key is already in use
	 */
	private function writeHeader() {
		$header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
436
		return parent::stream_write($header);
437
438
439
	}

}