Completed
Pull Request — master (#5629)
by Björn
17:14
created
apps/encryption/lib/Crypto/Encryption.php 1 patch
Indentation   +554 added lines, -554 removed lines patch added patch discarded remove patch
@@ -43,558 +43,558 @@
 block discarded – undo
43 43
 
44 44
 class Encryption implements IEncryptionModule {
45 45
 
46
-	const ID = 'OC_DEFAULT_MODULE';
47
-	const DISPLAY_NAME = 'Default encryption module';
48
-
49
-	/**
50
-	 * @var Crypt
51
-	 */
52
-	private $crypt;
53
-
54
-	/** @var string */
55
-	private $cipher;
56
-
57
-	/** @var string */
58
-	private $path;
59
-
60
-	/** @var string */
61
-	private $user;
62
-
63
-	/** @var  string */
64
-	private $owner;
65
-
66
-	/** @var string */
67
-	private $fileKey;
68
-
69
-	/** @var string */
70
-	private $writeCache;
71
-
72
-	/** @var KeyManager */
73
-	private $keyManager;
74
-
75
-	/** @var array */
76
-	private $accessList;
77
-
78
-	/** @var boolean */
79
-	private $isWriteOperation;
80
-
81
-	/** @var Util */
82
-	private $util;
83
-
84
-	/** @var  Session */
85
-	private $session;
86
-
87
-	/** @var  ILogger */
88
-	private $logger;
89
-
90
-	/** @var IL10N */
91
-	private $l;
92
-
93
-	/** @var EncryptAll */
94
-	private $encryptAll;
95
-
96
-	/** @var  bool */
97
-	private $useMasterPassword;
98
-
99
-	/** @var DecryptAll  */
100
-	private $decryptAll;
101
-
102
-	/** @var int unencrypted block size if block contains signature */
103
-	private $unencryptedBlockSizeSigned = 6072;
104
-
105
-	/** @var int unencrypted block size */
106
-	private $unencryptedBlockSize = 6126;
107
-
108
-	/** @var int Current version of the file */
109
-	private $version = 0;
110
-
111
-	/** @var array remember encryption signature version */
112
-	private static $rememberVersion = [];
113
-
114
-
115
-	/**
116
-	 *
117
-	 * @param Crypt $crypt
118
-	 * @param KeyManager $keyManager
119
-	 * @param Util $util
120
-	 * @param Session $session
121
-	 * @param EncryptAll $encryptAll
122
-	 * @param DecryptAll $decryptAll
123
-	 * @param ILogger $logger
124
-	 * @param IL10N $il10n
125
-	 */
126
-	public function __construct(Crypt $crypt,
127
-								KeyManager $keyManager,
128
-								Util $util,
129
-								Session $session,
130
-								EncryptAll $encryptAll,
131
-								DecryptAll $decryptAll,
132
-								ILogger $logger,
133
-								IL10N $il10n) {
134
-		$this->crypt = $crypt;
135
-		$this->keyManager = $keyManager;
136
-		$this->util = $util;
137
-		$this->session = $session;
138
-		$this->encryptAll = $encryptAll;
139
-		$this->decryptAll = $decryptAll;
140
-		$this->logger = $logger;
141
-		$this->l = $il10n;
142
-		$this->useMasterPassword = $util->isMasterKeyEnabled();
143
-	}
144
-
145
-	/**
146
-	 * @return string defining the technical unique id
147
-	 */
148
-	public function getId() {
149
-		return self::ID;
150
-	}
151
-
152
-	/**
153
-	 * In comparison to getKey() this function returns a human readable (maybe translated) name
154
-	 *
155
-	 * @return string
156
-	 */
157
-	public function getDisplayName() {
158
-		return self::DISPLAY_NAME;
159
-	}
160
-
161
-	/**
162
-	 * start receiving chunks from a file. This is the place where you can
163
-	 * perform some initial step before starting encrypting/decrypting the
164
-	 * chunks
165
-	 *
166
-	 * @param string $path to the file
167
-	 * @param string $user who read/write the file
168
-	 * @param string $mode php stream open mode
169
-	 * @param array $header contains the header data read from the file
170
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
171
-	 *
172
-	 * @return array $header contain data as key-value pairs which should be
173
-	 *                       written to the header, in case of a write operation
174
-	 *                       or if no additional data is needed return a empty array
175
-	 */
176
-	public function begin($path, $user, $mode, array $header, array $accessList) {
177
-		$this->path = $this->getPathToRealFile($path);
178
-		$this->accessList = $accessList;
179
-		$this->user = $user;
180
-		$this->owner = $this->getOwner($path);
181
-		$this->isWriteOperation = false;
182
-		$this->writeCache = '';
183
-
184
-		if($this->session->isReady() === false) {
185
-			// if the master key is enabled we can initialize encryption
186
-			// with a empty password and user name
187
-			if ($this->util->isMasterKeyEnabled()) {
188
-				$this->keyManager->init('', '');
189
-			}
190
-		}
191
-
192
-		if ($this->session->decryptAllModeActivated()) {
193
-			$encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
194
-			$shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
195
-			$this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
196
-				$shareKey,
197
-				$this->session->getDecryptAllKey());
198
-		} else {
199
-			$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
200
-		}
201
-
202
-		// always use the version from the original file, also part files
203
-		// need to have a correct version number if they get moved over to the
204
-		// final location
205
-		$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
206
-
207
-		if (
208
-			$mode === 'w'
209
-			|| $mode === 'w+'
210
-			|| $mode === 'wb'
211
-			|| $mode === 'wb+'
212
-		) {
213
-			$this->isWriteOperation = true;
214
-			if (empty($this->fileKey)) {
215
-				$this->fileKey = $this->crypt->generateFileKey();
216
-			}
217
-		} else {
218
-			// if we read a part file we need to increase the version by 1
219
-			// because the version number was also increased by writing
220
-			// the part file
221
-			if(Scanner::isPartialFile($path)) {
222
-				$this->version = $this->version + 1;
223
-			}
224
-		}
225
-
226
-		if ($this->isWriteOperation) {
227
-			$this->cipher = $this->crypt->getCipher();
228
-		} elseif (isset($header['cipher'])) {
229
-			$this->cipher = $header['cipher'];
230
-		} else {
231
-			// if we read a file without a header we fall-back to the legacy cipher
232
-			// which was used in <=oC6
233
-			$this->cipher = $this->crypt->getLegacyCipher();
234
-		}
235
-
236
-		return array('cipher' => $this->cipher, 'signed' => 'true');
237
-	}
238
-
239
-	/**
240
-	 * last chunk received. This is the place where you can perform some final
241
-	 * operation and return some remaining data if something is left in your
242
-	 * buffer.
243
-	 *
244
-	 * @param string $path to the file
245
-	 * @param int $position
246
-	 * @return string remained data which should be written to the file in case
247
-	 *                of a write operation
248
-	 * @throws PublicKeyMissingException
249
-	 * @throws \Exception
250
-	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
251
-	 */
252
-	public function end($path, $position = 0) {
253
-		$result = '';
254
-		if ($this->isWriteOperation) {
255
-			$this->keyManager->setVersion($path, $this->version + 1, new View());
256
-			// in case of a part file we remember the new signature versions
257
-			// the version will be set later on update.
258
-			// This way we make sure that other apps listening to the pre-hooks
259
-			// still get the old version which should be the correct value for them
260
-			if (Scanner::isPartialFile($path)) {
261
-				self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
262
-			}
263
-			if (!empty($this->writeCache)) {
264
-				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
265
-				$this->writeCache = '';
266
-			}
267
-			$publicKeys = array();
268
-			if ($this->useMasterPassword === true) {
269
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
270
-			} else {
271
-				foreach ($this->accessList['users'] as $uid) {
272
-					try {
273
-						$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
274
-					} catch (PublicKeyMissingException $e) {
275
-						$this->logger->warning(
276
-							'no public key found for user "{uid}", user will not be able to read the file',
277
-							['app' => 'encryption', 'uid' => $uid]
278
-						);
279
-						// if the public key of the owner is missing we should fail
280
-						if ($uid === $this->user) {
281
-							throw $e;
282
-						}
283
-					}
284
-				}
285
-			}
286
-
287
-			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner());
288
-			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
289
-			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
290
-		}
291
-		return $result;
292
-	}
293
-
294
-
295
-
296
-	/**
297
-	 * encrypt data
298
-	 *
299
-	 * @param string $data you want to encrypt
300
-	 * @param int $position
301
-	 * @return string encrypted data
302
-	 */
303
-	public function encrypt($data, $position = 0) {
304
-		// If extra data is left over from the last round, make sure it
305
-		// is integrated into the next block
306
-		if ($this->writeCache) {
307
-
308
-			// Concat writeCache to start of $data
309
-			$data = $this->writeCache . $data;
310
-
311
-			// Clear the write cache, ready for reuse - it has been
312
-			// flushed and its old contents processed
313
-			$this->writeCache = '';
314
-
315
-		}
316
-
317
-		$encrypted = '';
318
-		// While there still remains some data to be processed & written
319
-		while (strlen($data) > 0) {
320
-
321
-			// Remaining length for this iteration, not of the
322
-			// entire file (may be greater than 8192 bytes)
323
-			$remainingLength = strlen($data);
324
-
325
-			// If data remaining to be written is less than the
326
-			// size of 1 6126 byte block
327
-			if ($remainingLength < $this->unencryptedBlockSizeSigned) {
328
-
329
-				// Set writeCache to contents of $data
330
-				// The writeCache will be carried over to the
331
-				// next write round, and added to the start of
332
-				// $data to ensure that written blocks are
333
-				// always the correct length. If there is still
334
-				// data in writeCache after the writing round
335
-				// has finished, then the data will be written
336
-				// to disk by $this->flush().
337
-				$this->writeCache = $data;
338
-
339
-				// Clear $data ready for next round
340
-				$data = '';
341
-
342
-			} else {
343
-
344
-				// Read the chunk from the start of $data
345
-				$chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
346
-
347
-				$encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position);
348
-
349
-				// Remove the chunk we just processed from
350
-				// $data, leaving only unprocessed data in $data
351
-				// var, for handling on the next round
352
-				$data = substr($data, $this->unencryptedBlockSizeSigned);
353
-
354
-			}
355
-
356
-		}
357
-
358
-		return $encrypted;
359
-	}
360
-
361
-	/**
362
-	 * decrypt data
363
-	 *
364
-	 * @param string $data you want to decrypt
365
-	 * @param int $position
366
-	 * @return string decrypted data
367
-	 * @throws DecryptionFailedException
368
-	 */
369
-	public function decrypt($data, $position = 0) {
370
-		if (empty($this->fileKey)) {
371
-			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
372
-			$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.');
373
-			$this->logger->error($msg);
374
-
375
-			throw new DecryptionFailedException($msg, $hint);
376
-		}
377
-
378
-		return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
379
-	}
380
-
381
-	/**
382
-	 * update encrypted file, e.g. give additional users access to the file
383
-	 *
384
-	 * @param string $path path to the file which should be updated
385
-	 * @param string $uid of the user who performs the operation
386
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
387
-	 * @return boolean
388
-	 */
389
-	public function update($path, $uid, array $accessList) {
390
-
391
-		if (empty($accessList)) {
392
-			if (isset(self::$rememberVersion[$path])) {
393
-				$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
394
-				unset(self::$rememberVersion[$path]);
395
-			}
396
-			return;
397
-		}
398
-
399
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
400
-
401
-		if (!empty($fileKey)) {
402
-
403
-			$publicKeys = array();
404
-			if ($this->useMasterPassword === true) {
405
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
406
-			} else {
407
-				foreach ($accessList['users'] as $user) {
408
-					try {
409
-						$publicKeys[$user] = $this->keyManager->getPublicKey($user);
410
-					} catch (PublicKeyMissingException $e) {
411
-						$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
412
-					}
413
-				}
414
-			}
415
-
416
-			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
417
-
418
-			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
419
-
420
-			$this->keyManager->deleteAllFileKeys($path);
421
-
422
-			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
423
-
424
-		} else {
425
-			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
426
-				array('file' => $path, 'app' => 'encryption'));
427
-
428
-			return false;
429
-		}
430
-
431
-		return true;
432
-	}
433
-
434
-	/**
435
-	 * should the file be encrypted or not
436
-	 *
437
-	 * @param string $path
438
-	 * @return boolean
439
-	 */
440
-	public function shouldEncrypt($path) {
441
-		if ($this->util->shouldEncryptHomeStorage() === false) {
442
-			$storage = $this->util->getStorage($path);
443
-			if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
444
-				return false;
445
-			}
446
-		}
447
-		$parts = explode('/', $path);
448
-		if (count($parts) < 4) {
449
-			return false;
450
-		}
451
-
452
-		if ($parts[2] == 'files') {
453
-			return true;
454
-		}
455
-		if ($parts[2] == 'files_versions') {
456
-			return true;
457
-		}
458
-		if ($parts[2] == 'files_trashbin') {
459
-			return true;
460
-		}
461
-
462
-		return false;
463
-	}
464
-
465
-	/**
466
-	 * get size of the unencrypted payload per block.
467
-	 * Nextcloud read/write files with a block size of 8192 byte
468
-	 *
469
-	 * @param bool $signed
470
-	 * @return int
471
-	 */
472
-	public function getUnencryptedBlockSize($signed = false) {
473
-		if ($signed === false) {
474
-			return $this->unencryptedBlockSize;
475
-		}
476
-
477
-		return $this->unencryptedBlockSizeSigned;
478
-	}
479
-
480
-	/**
481
-	 * check if the encryption module is able to read the file,
482
-	 * e.g. if all encryption keys exists
483
-	 *
484
-	 * @param string $path
485
-	 * @param string $uid user for whom we want to check if he can read the file
486
-	 * @return bool
487
-	 * @throws DecryptionFailedException
488
-	 */
489
-	public function isReadable($path, $uid) {
490
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
491
-		if (empty($fileKey)) {
492
-			$owner = $this->util->getOwner($path);
493
-			if ($owner !== $uid) {
494
-				// if it is a shared file we throw a exception with a useful
495
-				// error message because in this case it means that the file was
496
-				// shared with the user at a point where the user didn't had a
497
-				// valid private/public key
498
-				$msg = 'Encryption module "' . $this->getDisplayName() .
499
-					'" is not able to read ' . $path;
500
-				$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.');
501
-				$this->logger->warning($msg);
502
-				throw new DecryptionFailedException($msg, $hint);
503
-			}
504
-			return false;
505
-		}
506
-
507
-		return true;
508
-	}
509
-
510
-	/**
511
-	 * Initial encryption of all files
512
-	 *
513
-	 * @param InputInterface $input
514
-	 * @param OutputInterface $output write some status information to the terminal during encryption
515
-	 */
516
-	public function encryptAll(InputInterface $input, OutputInterface $output) {
517
-		$this->encryptAll->encryptAll($input, $output);
518
-	}
519
-
520
-	/**
521
-	 * prepare module to perform decrypt all operation
522
-	 *
523
-	 * @param InputInterface $input
524
-	 * @param OutputInterface $output
525
-	 * @param string $user
526
-	 * @return bool
527
-	 */
528
-	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
529
-		return $this->decryptAll->prepare($input, $output, $user);
530
-	}
531
-
532
-
533
-	/**
534
-	 * @param string $path
535
-	 * @return string
536
-	 */
537
-	protected function getPathToRealFile($path) {
538
-		$realPath = $path;
539
-		$parts = explode('/', $path);
540
-		if ($parts[2] === 'files_versions') {
541
-			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
542
-			$length = strrpos($realPath, '.');
543
-			$realPath = substr($realPath, 0, $length);
544
-		}
545
-
546
-		return $realPath;
547
-	}
548
-
549
-	/**
550
-	 * remove .part file extension and the ocTransferId from the file to get the
551
-	 * original file name
552
-	 *
553
-	 * @param string $path
554
-	 * @return string
555
-	 */
556
-	protected function stripPartFileExtension($path) {
557
-		if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
558
-			$pos = strrpos($path, '.', -6);
559
-			$path = substr($path, 0, $pos);
560
-		}
561
-
562
-		return $path;
563
-	}
564
-
565
-	/**
566
-	 * get owner of a file
567
-	 *
568
-	 * @param string $path
569
-	 * @return string
570
-	 */
571
-	protected function getOwner($path) {
572
-		if ($this->owner === null) {
573
-			$this->owner = $this->util->getOwner($path);
574
-		}
575
-		return $this->owner;
576
-	}
577
-
578
-	/**
579
-	 * Check if the module is ready to be used by that specific user.
580
-	 * In case a module is not ready - because e.g. key pairs have not been generated
581
-	 * upon login this method can return false before any operation starts and might
582
-	 * cause issues during operations.
583
-	 *
584
-	 * @param string $user
585
-	 * @return boolean
586
-	 * @since 9.1.0
587
-	 */
588
-	public function isReadyForUser($user) {
589
-		return $this->keyManager->userHasKeys($user);
590
-	}
591
-
592
-	/**
593
-	 * We only need a detailed access list if the master key is not enabled
594
-	 *
595
-	 * @return bool
596
-	 */
597
-	public function needDetailedAccessList() {
598
-		return !$this->util->isMasterKeyEnabled();
599
-	}
46
+    const ID = 'OC_DEFAULT_MODULE';
47
+    const DISPLAY_NAME = 'Default encryption module';
48
+
49
+    /**
50
+     * @var Crypt
51
+     */
52
+    private $crypt;
53
+
54
+    /** @var string */
55
+    private $cipher;
56
+
57
+    /** @var string */
58
+    private $path;
59
+
60
+    /** @var string */
61
+    private $user;
62
+
63
+    /** @var  string */
64
+    private $owner;
65
+
66
+    /** @var string */
67
+    private $fileKey;
68
+
69
+    /** @var string */
70
+    private $writeCache;
71
+
72
+    /** @var KeyManager */
73
+    private $keyManager;
74
+
75
+    /** @var array */
76
+    private $accessList;
77
+
78
+    /** @var boolean */
79
+    private $isWriteOperation;
80
+
81
+    /** @var Util */
82
+    private $util;
83
+
84
+    /** @var  Session */
85
+    private $session;
86
+
87
+    /** @var  ILogger */
88
+    private $logger;
89
+
90
+    /** @var IL10N */
91
+    private $l;
92
+
93
+    /** @var EncryptAll */
94
+    private $encryptAll;
95
+
96
+    /** @var  bool */
97
+    private $useMasterPassword;
98
+
99
+    /** @var DecryptAll  */
100
+    private $decryptAll;
101
+
102
+    /** @var int unencrypted block size if block contains signature */
103
+    private $unencryptedBlockSizeSigned = 6072;
104
+
105
+    /** @var int unencrypted block size */
106
+    private $unencryptedBlockSize = 6126;
107
+
108
+    /** @var int Current version of the file */
109
+    private $version = 0;
110
+
111
+    /** @var array remember encryption signature version */
112
+    private static $rememberVersion = [];
113
+
114
+
115
+    /**
116
+     *
117
+     * @param Crypt $crypt
118
+     * @param KeyManager $keyManager
119
+     * @param Util $util
120
+     * @param Session $session
121
+     * @param EncryptAll $encryptAll
122
+     * @param DecryptAll $decryptAll
123
+     * @param ILogger $logger
124
+     * @param IL10N $il10n
125
+     */
126
+    public function __construct(Crypt $crypt,
127
+                                KeyManager $keyManager,
128
+                                Util $util,
129
+                                Session $session,
130
+                                EncryptAll $encryptAll,
131
+                                DecryptAll $decryptAll,
132
+                                ILogger $logger,
133
+                                IL10N $il10n) {
134
+        $this->crypt = $crypt;
135
+        $this->keyManager = $keyManager;
136
+        $this->util = $util;
137
+        $this->session = $session;
138
+        $this->encryptAll = $encryptAll;
139
+        $this->decryptAll = $decryptAll;
140
+        $this->logger = $logger;
141
+        $this->l = $il10n;
142
+        $this->useMasterPassword = $util->isMasterKeyEnabled();
143
+    }
144
+
145
+    /**
146
+     * @return string defining the technical unique id
147
+     */
148
+    public function getId() {
149
+        return self::ID;
150
+    }
151
+
152
+    /**
153
+     * In comparison to getKey() this function returns a human readable (maybe translated) name
154
+     *
155
+     * @return string
156
+     */
157
+    public function getDisplayName() {
158
+        return self::DISPLAY_NAME;
159
+    }
160
+
161
+    /**
162
+     * start receiving chunks from a file. This is the place where you can
163
+     * perform some initial step before starting encrypting/decrypting the
164
+     * chunks
165
+     *
166
+     * @param string $path to the file
167
+     * @param string $user who read/write the file
168
+     * @param string $mode php stream open mode
169
+     * @param array $header contains the header data read from the file
170
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
171
+     *
172
+     * @return array $header contain data as key-value pairs which should be
173
+     *                       written to the header, in case of a write operation
174
+     *                       or if no additional data is needed return a empty array
175
+     */
176
+    public function begin($path, $user, $mode, array $header, array $accessList) {
177
+        $this->path = $this->getPathToRealFile($path);
178
+        $this->accessList = $accessList;
179
+        $this->user = $user;
180
+        $this->owner = $this->getOwner($path);
181
+        $this->isWriteOperation = false;
182
+        $this->writeCache = '';
183
+
184
+        if($this->session->isReady() === false) {
185
+            // if the master key is enabled we can initialize encryption
186
+            // with a empty password and user name
187
+            if ($this->util->isMasterKeyEnabled()) {
188
+                $this->keyManager->init('', '');
189
+            }
190
+        }
191
+
192
+        if ($this->session->decryptAllModeActivated()) {
193
+            $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
194
+            $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
195
+            $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
196
+                $shareKey,
197
+                $this->session->getDecryptAllKey());
198
+        } else {
199
+            $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
200
+        }
201
+
202
+        // always use the version from the original file, also part files
203
+        // need to have a correct version number if they get moved over to the
204
+        // final location
205
+        $this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
206
+
207
+        if (
208
+            $mode === 'w'
209
+            || $mode === 'w+'
210
+            || $mode === 'wb'
211
+            || $mode === 'wb+'
212
+        ) {
213
+            $this->isWriteOperation = true;
214
+            if (empty($this->fileKey)) {
215
+                $this->fileKey = $this->crypt->generateFileKey();
216
+            }
217
+        } else {
218
+            // if we read a part file we need to increase the version by 1
219
+            // because the version number was also increased by writing
220
+            // the part file
221
+            if(Scanner::isPartialFile($path)) {
222
+                $this->version = $this->version + 1;
223
+            }
224
+        }
225
+
226
+        if ($this->isWriteOperation) {
227
+            $this->cipher = $this->crypt->getCipher();
228
+        } elseif (isset($header['cipher'])) {
229
+            $this->cipher = $header['cipher'];
230
+        } else {
231
+            // if we read a file without a header we fall-back to the legacy cipher
232
+            // which was used in <=oC6
233
+            $this->cipher = $this->crypt->getLegacyCipher();
234
+        }
235
+
236
+        return array('cipher' => $this->cipher, 'signed' => 'true');
237
+    }
238
+
239
+    /**
240
+     * last chunk received. This is the place where you can perform some final
241
+     * operation and return some remaining data if something is left in your
242
+     * buffer.
243
+     *
244
+     * @param string $path to the file
245
+     * @param int $position
246
+     * @return string remained data which should be written to the file in case
247
+     *                of a write operation
248
+     * @throws PublicKeyMissingException
249
+     * @throws \Exception
250
+     * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
251
+     */
252
+    public function end($path, $position = 0) {
253
+        $result = '';
254
+        if ($this->isWriteOperation) {
255
+            $this->keyManager->setVersion($path, $this->version + 1, new View());
256
+            // in case of a part file we remember the new signature versions
257
+            // the version will be set later on update.
258
+            // This way we make sure that other apps listening to the pre-hooks
259
+            // still get the old version which should be the correct value for them
260
+            if (Scanner::isPartialFile($path)) {
261
+                self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
262
+            }
263
+            if (!empty($this->writeCache)) {
264
+                $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
265
+                $this->writeCache = '';
266
+            }
267
+            $publicKeys = array();
268
+            if ($this->useMasterPassword === true) {
269
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
270
+            } else {
271
+                foreach ($this->accessList['users'] as $uid) {
272
+                    try {
273
+                        $publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
274
+                    } catch (PublicKeyMissingException $e) {
275
+                        $this->logger->warning(
276
+                            'no public key found for user "{uid}", user will not be able to read the file',
277
+                            ['app' => 'encryption', 'uid' => $uid]
278
+                        );
279
+                        // if the public key of the owner is missing we should fail
280
+                        if ($uid === $this->user) {
281
+                            throw $e;
282
+                        }
283
+                    }
284
+                }
285
+            }
286
+
287
+            $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner());
288
+            $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
289
+            $this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
290
+        }
291
+        return $result;
292
+    }
293
+
294
+
295
+
296
+    /**
297
+     * encrypt data
298
+     *
299
+     * @param string $data you want to encrypt
300
+     * @param int $position
301
+     * @return string encrypted data
302
+     */
303
+    public function encrypt($data, $position = 0) {
304
+        // If extra data is left over from the last round, make sure it
305
+        // is integrated into the next block
306
+        if ($this->writeCache) {
307
+
308
+            // Concat writeCache to start of $data
309
+            $data = $this->writeCache . $data;
310
+
311
+            // Clear the write cache, ready for reuse - it has been
312
+            // flushed and its old contents processed
313
+            $this->writeCache = '';
314
+
315
+        }
316
+
317
+        $encrypted = '';
318
+        // While there still remains some data to be processed & written
319
+        while (strlen($data) > 0) {
320
+
321
+            // Remaining length for this iteration, not of the
322
+            // entire file (may be greater than 8192 bytes)
323
+            $remainingLength = strlen($data);
324
+
325
+            // If data remaining to be written is less than the
326
+            // size of 1 6126 byte block
327
+            if ($remainingLength < $this->unencryptedBlockSizeSigned) {
328
+
329
+                // Set writeCache to contents of $data
330
+                // The writeCache will be carried over to the
331
+                // next write round, and added to the start of
332
+                // $data to ensure that written blocks are
333
+                // always the correct length. If there is still
334
+                // data in writeCache after the writing round
335
+                // has finished, then the data will be written
336
+                // to disk by $this->flush().
337
+                $this->writeCache = $data;
338
+
339
+                // Clear $data ready for next round
340
+                $data = '';
341
+
342
+            } else {
343
+
344
+                // Read the chunk from the start of $data
345
+                $chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
346
+
347
+                $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position);
348
+
349
+                // Remove the chunk we just processed from
350
+                // $data, leaving only unprocessed data in $data
351
+                // var, for handling on the next round
352
+                $data = substr($data, $this->unencryptedBlockSizeSigned);
353
+
354
+            }
355
+
356
+        }
357
+
358
+        return $encrypted;
359
+    }
360
+
361
+    /**
362
+     * decrypt data
363
+     *
364
+     * @param string $data you want to decrypt
365
+     * @param int $position
366
+     * @return string decrypted data
367
+     * @throws DecryptionFailedException
368
+     */
369
+    public function decrypt($data, $position = 0) {
370
+        if (empty($this->fileKey)) {
371
+            $msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
372
+            $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.');
373
+            $this->logger->error($msg);
374
+
375
+            throw new DecryptionFailedException($msg, $hint);
376
+        }
377
+
378
+        return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
379
+    }
380
+
381
+    /**
382
+     * update encrypted file, e.g. give additional users access to the file
383
+     *
384
+     * @param string $path path to the file which should be updated
385
+     * @param string $uid of the user who performs the operation
386
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
387
+     * @return boolean
388
+     */
389
+    public function update($path, $uid, array $accessList) {
390
+
391
+        if (empty($accessList)) {
392
+            if (isset(self::$rememberVersion[$path])) {
393
+                $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
394
+                unset(self::$rememberVersion[$path]);
395
+            }
396
+            return;
397
+        }
398
+
399
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
400
+
401
+        if (!empty($fileKey)) {
402
+
403
+            $publicKeys = array();
404
+            if ($this->useMasterPassword === true) {
405
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
406
+            } else {
407
+                foreach ($accessList['users'] as $user) {
408
+                    try {
409
+                        $publicKeys[$user] = $this->keyManager->getPublicKey($user);
410
+                    } catch (PublicKeyMissingException $e) {
411
+                        $this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
412
+                    }
413
+                }
414
+            }
415
+
416
+            $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
417
+
418
+            $encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
419
+
420
+            $this->keyManager->deleteAllFileKeys($path);
421
+
422
+            $this->keyManager->setAllFileKeys($path, $encryptedFileKey);
423
+
424
+        } else {
425
+            $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
426
+                array('file' => $path, 'app' => 'encryption'));
427
+
428
+            return false;
429
+        }
430
+
431
+        return true;
432
+    }
433
+
434
+    /**
435
+     * should the file be encrypted or not
436
+     *
437
+     * @param string $path
438
+     * @return boolean
439
+     */
440
+    public function shouldEncrypt($path) {
441
+        if ($this->util->shouldEncryptHomeStorage() === false) {
442
+            $storage = $this->util->getStorage($path);
443
+            if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
444
+                return false;
445
+            }
446
+        }
447
+        $parts = explode('/', $path);
448
+        if (count($parts) < 4) {
449
+            return false;
450
+        }
451
+
452
+        if ($parts[2] == 'files') {
453
+            return true;
454
+        }
455
+        if ($parts[2] == 'files_versions') {
456
+            return true;
457
+        }
458
+        if ($parts[2] == 'files_trashbin') {
459
+            return true;
460
+        }
461
+
462
+        return false;
463
+    }
464
+
465
+    /**
466
+     * get size of the unencrypted payload per block.
467
+     * Nextcloud read/write files with a block size of 8192 byte
468
+     *
469
+     * @param bool $signed
470
+     * @return int
471
+     */
472
+    public function getUnencryptedBlockSize($signed = false) {
473
+        if ($signed === false) {
474
+            return $this->unencryptedBlockSize;
475
+        }
476
+
477
+        return $this->unencryptedBlockSizeSigned;
478
+    }
479
+
480
+    /**
481
+     * check if the encryption module is able to read the file,
482
+     * e.g. if all encryption keys exists
483
+     *
484
+     * @param string $path
485
+     * @param string $uid user for whom we want to check if he can read the file
486
+     * @return bool
487
+     * @throws DecryptionFailedException
488
+     */
489
+    public function isReadable($path, $uid) {
490
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
491
+        if (empty($fileKey)) {
492
+            $owner = $this->util->getOwner($path);
493
+            if ($owner !== $uid) {
494
+                // if it is a shared file we throw a exception with a useful
495
+                // error message because in this case it means that the file was
496
+                // shared with the user at a point where the user didn't had a
497
+                // valid private/public key
498
+                $msg = 'Encryption module "' . $this->getDisplayName() .
499
+                    '" is not able to read ' . $path;
500
+                $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.');
501
+                $this->logger->warning($msg);
502
+                throw new DecryptionFailedException($msg, $hint);
503
+            }
504
+            return false;
505
+        }
506
+
507
+        return true;
508
+    }
509
+
510
+    /**
511
+     * Initial encryption of all files
512
+     *
513
+     * @param InputInterface $input
514
+     * @param OutputInterface $output write some status information to the terminal during encryption
515
+     */
516
+    public function encryptAll(InputInterface $input, OutputInterface $output) {
517
+        $this->encryptAll->encryptAll($input, $output);
518
+    }
519
+
520
+    /**
521
+     * prepare module to perform decrypt all operation
522
+     *
523
+     * @param InputInterface $input
524
+     * @param OutputInterface $output
525
+     * @param string $user
526
+     * @return bool
527
+     */
528
+    public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
529
+        return $this->decryptAll->prepare($input, $output, $user);
530
+    }
531
+
532
+
533
+    /**
534
+     * @param string $path
535
+     * @return string
536
+     */
537
+    protected function getPathToRealFile($path) {
538
+        $realPath = $path;
539
+        $parts = explode('/', $path);
540
+        if ($parts[2] === 'files_versions') {
541
+            $realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
542
+            $length = strrpos($realPath, '.');
543
+            $realPath = substr($realPath, 0, $length);
544
+        }
545
+
546
+        return $realPath;
547
+    }
548
+
549
+    /**
550
+     * remove .part file extension and the ocTransferId from the file to get the
551
+     * original file name
552
+     *
553
+     * @param string $path
554
+     * @return string
555
+     */
556
+    protected function stripPartFileExtension($path) {
557
+        if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
558
+            $pos = strrpos($path, '.', -6);
559
+            $path = substr($path, 0, $pos);
560
+        }
561
+
562
+        return $path;
563
+    }
564
+
565
+    /**
566
+     * get owner of a file
567
+     *
568
+     * @param string $path
569
+     * @return string
570
+     */
571
+    protected function getOwner($path) {
572
+        if ($this->owner === null) {
573
+            $this->owner = $this->util->getOwner($path);
574
+        }
575
+        return $this->owner;
576
+    }
577
+
578
+    /**
579
+     * Check if the module is ready to be used by that specific user.
580
+     * In case a module is not ready - because e.g. key pairs have not been generated
581
+     * upon login this method can return false before any operation starts and might
582
+     * cause issues during operations.
583
+     *
584
+     * @param string $user
585
+     * @return boolean
586
+     * @since 9.1.0
587
+     */
588
+    public function isReadyForUser($user) {
589
+        return $this->keyManager->userHasKeys($user);
590
+    }
591
+
592
+    /**
593
+     * We only need a detailed access list if the master key is not enabled
594
+     *
595
+     * @return bool
596
+     */
597
+    public function needDetailedAccessList() {
598
+        return !$this->util->isMasterKeyEnabled();
599
+    }
600 600
 }
Please login to merge, or discard this patch.