Completed
Push — stable8.1 ( 1d6099...c6eee6 )
by Morris
88:22
created

Encryption::getId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * @author Björn Schießle <[email protected]>
4
 * @author Clark Tomlinson <[email protected]>
5
 * @author Jan-Christoph Borchardt <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Thomas Müller <[email protected]>
10
 *
11
 * @copyright Copyright (c) 2015, ownCloud, Inc.
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OCA\Encryption\Crypto;
29
30
31
use OC\Encryption\Exceptions\DecryptionFailedException;
32
use OCA\Encryption\Exceptions\PublicKeyMissingException;
33
use OCA\Encryption\Util;
34
use OCP\Encryption\IEncryptionModule;
35
use OCA\Encryption\KeyManager;
36
use OCP\IL10N;
37
use OCP\ILogger;
38
39
class Encryption implements IEncryptionModule {
40
41
	const ID = 'OC_DEFAULT_MODULE';
42
	const DISPLAY_NAME = 'Default encryption module';
43
44
	/**
45
	 * @var Crypt
46
	 */
47
	private $crypt;
48
49
	/** @var string */
50
	private $cipher;
51
52
	/** @var string */
53
	private $path;
54
55
	/** @var string */
56
	private $user;
57
58
	/** @var string */
59
	private $fileKey;
60
61
	/** @var string */
62
	private $writeCache;
63
64
	/** @var KeyManager */
65
	private $keyManager;
66
67
	/** @var array */
68
	private $accessList;
69
70
	/** @var boolean */
71
	private $isWriteOperation;
72
73
	/** @var Util */
74
	private $util;
75
76
	/** @var  ILogger */
77
	private $logger;
78
79
	/** @var IL10N */
80
	private $l;
81
82
	/**
83
	 *
84
	 * @param Crypt $crypt
85
	 * @param KeyManager $keyManager
86
	 * @param Util $util
87
	 * @param ILogger $logger
88
	 * @param IL10N $il10n
89
	 */
90 22
	public function __construct(Crypt $crypt,
91
								KeyManager $keyManager,
92
								Util $util,
93
								ILogger $logger,
94
								IL10N $il10n) {
95 22
		$this->crypt = $crypt;
96 22
		$this->keyManager = $keyManager;
97 22
		$this->util = $util;
98 22
		$this->logger = $logger;
99 22
		$this->l = $il10n;
100 22
	}
101
102
	/**
103
	 * @return string defining the technical unique id
104
	 */
105
	public function getId() {
106
		return self::ID;
107
	}
108
109
	/**
110
	 * In comparison to getKey() this function returns a human readable (maybe translated) name
111
	 *
112
	 * @return string
113
	 */
114
	public function getDisplayName() {
115
		return self::DISPLAY_NAME;
116
	}
117
118
	/**
119
	 * start receiving chunks from a file. This is the place where you can
120
	 * perform some initial step before starting encrypting/decrypting the
121
	 * chunks
122
	 *
123
	 * @param string $path to the file
124
	 * @param string $user who read/write the file
125
	 * @param string $mode php stream open mode
126
	 * @param array $header contains the header data read from the file
127
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
128
	 *
129
	 * @return array $header contain data as key-value pairs which should be
130
	 *                       written to the header, in case of a write operation
131
	 *                       or if no additional data is needed return a empty array
132
	 */
133 6
	public function begin($path, $user, $mode, array $header, array $accessList) {
134
135 6
		$this->path = $this->getPathToRealFile($path);
136 6
		$this->accessList = $accessList;
137 6
		$this->user = $user;
138 6
		$this->isWriteOperation = false;
139 6
		$this->writeCache = '';
140
141 6
		$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
142
143
		if (
144
			$mode === 'w'
145 6
			|| $mode === 'w+'
146 4
			|| $mode === 'wb'
147 4
			|| $mode === 'wb+'
148 6
		) {
149 2
			$this->isWriteOperation = true;
150 2
			if (empty($this->fileKey)) {
151 1
				$this->fileKey = $this->crypt->generateFileKey();
152 1
			}
153 2
		}
154
155 6
		if (isset($header['cipher'])) {
156 2
			$this->cipher = $header['cipher'];
157 6
		} elseif ($this->isWriteOperation) {
158 1
			$this->cipher = $this->crypt->getCipher();
159 1
		} else {
160
			// if we read a file without a header we fall-back to the legacy cipher
161
			// which was used in <=oC6
162 3
			$this->cipher = $this->crypt->getLegacyCipher();
163
		}
164
165 6
		return array('cipher' => $this->cipher);
166
	}
167
168
	/**
169
	 * last chunk received. This is the place where you can perform some final
170
	 * operation and return some remaining data if something is left in your
171
	 * buffer.
172
	 *
173
	 * @param string $path to the file
174
	 * @return string remained data which should be written to the file in case
175
	 *                of a write operation
176
	 * @throws PublicKeyMissingException
177
	 * @throws \Exception
178
	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
179
	 */
180 2
	public function end($path) {
181 2
		$result = '';
182 2
		if ($this->isWriteOperation) {
183 2
			if (!empty($this->writeCache)) {
184
				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->crypt->symmetricE...Cache, $this->fileKey); of type false|string adds false to the return on line 208 which is incompatible with the return type declared by the interface OCP\Encryption\IEncryptionModule::end of type string. It seems like you forgot to handle an error condition.
Loading history...
185
				$this->writeCache = '';
186
			}
187 2
			$publicKeys = array();
188 2
			foreach ($this->accessList['users'] as $uid) {
189
				try {
190 2
					$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
191 2
				} catch (PublicKeyMissingException $e) {
192 2
					$this->logger->warning(
193 2
						'no public key found for user "{uid}", user will not be able to read the file',
194 2
						['app' => 'encryption', 'uid' => $uid]
195 2
					);
196
					// if the public key of the owner is missing we should fail
197 2
					if ($uid === $this->user) {
198 1
						throw $e;
199
					}
200
				}
201 2
			}
202
203 1
			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->user);
204
205 1
			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
206 1
			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
207 1
		}
208 1
		return $result;
209
	}
210
211
	/**
212
	 * encrypt data
213
	 *
214
	 * @param string $data you want to encrypt
215
	 * @return mixed encrypted data
216
	 */
217
	public function encrypt($data) {
218
219
		// If extra data is left over from the last round, make sure it
220
		// is integrated into the next 6126 / 8192 block
221
		if ($this->writeCache) {
222
223
			// Concat writeCache to start of $data
224
			$data = $this->writeCache . $data;
225
226
			// Clear the write cache, ready for reuse - it has been
227
			// flushed and its old contents processed
228
			$this->writeCache = '';
229
230
		}
231
232
		$encrypted = '';
233
		// While there still remains some data to be processed & written
234
		while (strlen($data) > 0) {
235
236
			// Remaining length for this iteration, not of the
237
			// entire file (may be greater than 8192 bytes)
238
			$remainingLength = strlen($data);
239
240
			// If data remaining to be written is less than the
241
			// size of 1 6126 byte block
242
			if ($remainingLength < 6126) {
243
244
				// Set writeCache to contents of $data
245
				// The writeCache will be carried over to the
246
				// next write round, and added to the start of
247
				// $data to ensure that written blocks are
248
				// always the correct length. If there is still
249
				// data in writeCache after the writing round
250
				// has finished, then the data will be written
251
				// to disk by $this->flush().
252
				$this->writeCache = $data;
253
254
				// Clear $data ready for next round
255
				$data = '';
256
257
			} else {
258
259
				// Read the chunk from the start of $data
260
				$chunk = substr($data, 0, 6126);
261
262
				$encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey);
263
264
				// Remove the chunk we just processed from
265
				// $data, leaving only unprocessed data in $data
266
				// var, for handling on the next round
267
				$data = substr($data, 6126);
268
269
			}
270
271
		}
272
273
		return $encrypted;
274
	}
275
276
	/**
277
	 * decrypt data
278
	 *
279
	 * @param string $data you want to decrypt
280
	 * @return mixed decrypted data
281
	 * @throws DecryptionFailedException
282
	 */
283 1
	public function decrypt($data) {
284 1
		if (empty($this->fileKey)) {
285 1
			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
286 1
			$hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
287 1
			$this->logger->error($msg);
288
289 1
			throw new DecryptionFailedException($msg, $hint);
290
		}
291
292
		$result = '';
293
		if (!empty($data)) {
294
			$result = $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher);
295
		}
296
		return $result;
297
	}
298
299
	/**
300
	 * update encrypted file, e.g. give additional users access to the file
301
	 *
302
	 * @param string $path path to the file which should be updated
303
	 * @param string $uid of the user who performs the operation
304
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
305
	 * @return boolean
306
	 */
307 3
	public function update($path, $uid, array $accessList) {
308 3
		$fileKey = $this->keyManager->getFileKey($path, $uid);
309
310 3
		if (!empty($fileKey)) {
311
312 2
			$publicKeys = array();
313 2
			foreach ($accessList['users'] as $user) {
314
				try {
315 2
					$publicKeys[$user] = $this->keyManager->getPublicKey($user);
316 2
				} catch (PublicKeyMissingException $e) {
317 1
					$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
318
				}
319 2
			}
320
321 2
			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $uid);
322
323 2
			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
324
325 2
			$this->keyManager->deleteAllFileKeys($path);
326
327 2
			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
328
329 2
		} else {
330 1
			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
331 1
				array('file' => $path, 'app' => 'encryption'));
332
333 1
			return false;
334
		}
335
336 2
		return true;
337
	}
338
339
	/**
340
	 * should the file be encrypted or not
341
	 *
342
	 * @param string $path
343
	 * @return boolean
344
	 */
345 8
	public function shouldEncrypt($path) {
346 8
		$parts = explode('/', $path);
347 8
		if (count($parts) < 4) {
348 4
			return false;
349
		}
350
351 4
		if ($parts[2] == 'files') {
352 1
			return true;
353
		}
354 3
		if ($parts[2] == 'files_versions') {
355 1
			return true;
356
		}
357 2
		if ($parts[2] == 'files_trashbin') {
358 1
			return true;
359
		}
360
361 1
		return false;
362
	}
363
364
	/**
365
	 * get size of the unencrypted payload per block.
366
	 * ownCloud read/write files with a block size of 8192 byte
367
	 *
368
	 * @return integer
369
	 */
370
	public function getUnencryptedBlockSize() {
371
		return 6126;
372
	}
373
374
	/**
375
	 * check if the encryption module is able to read the file,
376
	 * e.g. if all encryption keys exists
377
	 *
378
	 * @param string $path
379
	 * @param string $uid user for whom we want to check if he can read the file
380
	 * @return bool
381
	 * @throws DecryptionFailedException
382
	 */
383
	public function isReadable($path, $uid) {
384
		$fileKey = $this->keyManager->getFileKey($path, $uid);
385
		if (empty($fileKey)) {
386
			$owner = $this->util->getOwner($path);
387
			if ($owner !== $uid) {
388
				// if it is a shared file we throw a exception with a useful
389
				// error message because in this case it means that the file was
390
				// shared with the user at a point where the user didn't had a
391
				// valid private/public key
392
				$msg = 'Encryption module "' . $this->getDisplayName() .
393
					'" is not able to read ' . $path;
394
				$hint = $this->l->t('Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
395
				$this->logger->warning($msg);
396
				throw new DecryptionFailedException($msg, $hint);
397
			}
398
			return false;
399
		}
400
401
		return true;
402
	}
403
404
	/**
405
	 * @param string $path
406
	 * @return string
407
	 */
408 10
	protected function getPathToRealFile($path) {
409 10
		$realPath = $path;
410 10
		$parts = explode('/', $path);
411 10
		if ($parts[2] === 'files_versions') {
412 2
			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
413 2
			$length = strrpos($realPath, '.');
414 2
			$realPath = substr($realPath, 0, $length);
415 2
		}
416
417 10
		return $realPath;
418
	}
419
420
}
421