Passed
Push — master ( e2d223...776c65 )
by Morris
40:15 queued 27:24
created
apps/encryption/lib/Crypto/Encryption.php 1 patch
Indentation   +549 added lines, -549 removed lines patch added patch discarded remove patch
@@ -46,553 +46,553 @@
 block discarded – undo
46 46
 use Symfony\Component\Console\Output\OutputInterface;
47 47
 
48 48
 class Encryption implements IEncryptionModule {
49
-	public const ID = 'OC_DEFAULT_MODULE';
50
-	public const DISPLAY_NAME = 'Default encryption module';
51
-
52
-	/**
53
-	 * @var Crypt
54
-	 */
55
-	private $crypt;
56
-
57
-	/** @var string */
58
-	private $cipher;
59
-
60
-	/** @var string */
61
-	private $path;
62
-
63
-	/** @var string */
64
-	private $user;
65
-
66
-	/** @var  array */
67
-	private $owner;
68
-
69
-	/** @var string */
70
-	private $fileKey;
71
-
72
-	/** @var string */
73
-	private $writeCache;
74
-
75
-	/** @var KeyManager */
76
-	private $keyManager;
77
-
78
-	/** @var array */
79
-	private $accessList;
80
-
81
-	/** @var boolean */
82
-	private $isWriteOperation;
83
-
84
-	/** @var Util */
85
-	private $util;
86
-
87
-	/** @var  Session */
88
-	private $session;
89
-
90
-	/** @var  ILogger */
91
-	private $logger;
92
-
93
-	/** @var IL10N */
94
-	private $l;
95
-
96
-	/** @var EncryptAll */
97
-	private $encryptAll;
98
-
99
-	/** @var  bool */
100
-	private $useMasterPassword;
101
-
102
-	/** @var DecryptAll  */
103
-	private $decryptAll;
104
-
105
-	/** @var int unencrypted block size if block contains signature */
106
-	private $unencryptedBlockSizeSigned = 6072;
107
-
108
-	/** @var int unencrypted block size */
109
-	private $unencryptedBlockSize = 6126;
110
-
111
-	/** @var int Current version of the file */
112
-	private $version = 0;
113
-
114
-	/** @var array remember encryption signature version */
115
-	private static $rememberVersion = [];
116
-
117
-
118
-	/**
119
-	 *
120
-	 * @param Crypt $crypt
121
-	 * @param KeyManager $keyManager
122
-	 * @param Util $util
123
-	 * @param Session $session
124
-	 * @param EncryptAll $encryptAll
125
-	 * @param DecryptAll $decryptAll
126
-	 * @param ILogger $logger
127
-	 * @param IL10N $il10n
128
-	 */
129
-	public function __construct(Crypt $crypt,
130
-								KeyManager $keyManager,
131
-								Util $util,
132
-								Session $session,
133
-								EncryptAll $encryptAll,
134
-								DecryptAll $decryptAll,
135
-								ILogger $logger,
136
-								IL10N $il10n) {
137
-		$this->crypt = $crypt;
138
-		$this->keyManager = $keyManager;
139
-		$this->util = $util;
140
-		$this->session = $session;
141
-		$this->encryptAll = $encryptAll;
142
-		$this->decryptAll = $decryptAll;
143
-		$this->logger = $logger;
144
-		$this->l = $il10n;
145
-		$this->owner = [];
146
-		$this->useMasterPassword = $util->isMasterKeyEnabled();
147
-	}
148
-
149
-	/**
150
-	 * @return string defining the technical unique id
151
-	 */
152
-	public function getId() {
153
-		return self::ID;
154
-	}
155
-
156
-	/**
157
-	 * In comparison to getKey() this function returns a human readable (maybe translated) name
158
-	 *
159
-	 * @return string
160
-	 */
161
-	public function getDisplayName() {
162
-		return self::DISPLAY_NAME;
163
-	}
164
-
165
-	/**
166
-	 * start receiving chunks from a file. This is the place where you can
167
-	 * perform some initial step before starting encrypting/decrypting the
168
-	 * chunks
169
-	 *
170
-	 * @param string $path to the file
171
-	 * @param string $user who read/write the file
172
-	 * @param string $mode php stream open mode
173
-	 * @param array $header contains the header data read from the file
174
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
175
-	 *
176
-	 * @return array $header contain data as key-value pairs which should be
177
-	 *                       written to the header, in case of a write operation
178
-	 *                       or if no additional data is needed return a empty array
179
-	 */
180
-	public function begin($path, $user, $mode, array $header, array $accessList) {
181
-		$this->path = $this->getPathToRealFile($path);
182
-		$this->accessList = $accessList;
183
-		$this->user = $user;
184
-		$this->isWriteOperation = false;
185
-		$this->writeCache = '';
186
-
187
-		if ($this->session->isReady() === false) {
188
-			// if the master key is enabled we can initialize encryption
189
-			// with a empty password and user name
190
-			if ($this->util->isMasterKeyEnabled()) {
191
-				$this->keyManager->init('', '');
192
-			}
193
-		}
194
-
195
-		if ($this->session->decryptAllModeActivated()) {
196
-			$encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
197
-			$shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
198
-			$this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
199
-				$shareKey,
200
-				$this->session->getDecryptAllKey());
201
-		} else {
202
-			$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
203
-		}
204
-
205
-		// always use the version from the original file, also part files
206
-		// need to have a correct version number if they get moved over to the
207
-		// final location
208
-		$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
209
-
210
-		if (
211
-			$mode === 'w'
212
-			|| $mode === 'w+'
213
-			|| $mode === 'wb'
214
-			|| $mode === 'wb+'
215
-		) {
216
-			$this->isWriteOperation = true;
217
-			if (empty($this->fileKey)) {
218
-				$this->fileKey = $this->crypt->generateFileKey();
219
-			}
220
-		} else {
221
-			// if we read a part file we need to increase the version by 1
222
-			// because the version number was also increased by writing
223
-			// the part file
224
-			if (Scanner::isPartialFile($path)) {
225
-				$this->version = $this->version + 1;
226
-			}
227
-		}
228
-
229
-		if ($this->isWriteOperation) {
230
-			$this->cipher = $this->crypt->getCipher();
231
-		} elseif (isset($header['cipher'])) {
232
-			$this->cipher = $header['cipher'];
233
-		} else {
234
-			// if we read a file without a header we fall-back to the legacy cipher
235
-			// which was used in <=oC6
236
-			$this->cipher = $this->crypt->getLegacyCipher();
237
-		}
238
-
239
-		return ['cipher' => $this->cipher, 'signed' => 'true'];
240
-	}
241
-
242
-	/**
243
-	 * last chunk received. This is the place where you can perform some final
244
-	 * operation and return some remaining data if something is left in your
245
-	 * buffer.
246
-	 *
247
-	 * @param string $path to the file
248
-	 * @param int $position
249
-	 * @return string remained data which should be written to the file in case
250
-	 *                of a write operation
251
-	 * @throws PublicKeyMissingException
252
-	 * @throws \Exception
253
-	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
254
-	 */
255
-	public function end($path, $position = 0) {
256
-		$result = '';
257
-		if ($this->isWriteOperation) {
258
-			// in case of a part file we remember the new signature versions
259
-			// the version will be set later on update.
260
-			// This way we make sure that other apps listening to the pre-hooks
261
-			// still get the old version which should be the correct value for them
262
-			if (Scanner::isPartialFile($path)) {
263
-				self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
264
-			}
265
-			if (!empty($this->writeCache)) {
266
-				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
267
-				$this->writeCache = '';
268
-			}
269
-			$publicKeys = [];
270
-			if ($this->useMasterPassword === true) {
271
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
272
-			} else {
273
-				foreach ($this->accessList['users'] as $uid) {
274
-					try {
275
-						$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
276
-					} catch (PublicKeyMissingException $e) {
277
-						$this->logger->warning(
278
-							'no public key found for user "{uid}", user will not be able to read the file',
279
-							['app' => 'encryption', 'uid' => $uid]
280
-						);
281
-						// if the public key of the owner is missing we should fail
282
-						if ($uid === $this->user) {
283
-							throw $e;
284
-						}
285
-					}
286
-				}
287
-			}
288
-
289
-			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path));
290
-			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
291
-			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
292
-		}
293
-		return $result;
294
-	}
295
-
296
-
297
-
298
-	/**
299
-	 * encrypt data
300
-	 *
301
-	 * @param string $data you want to encrypt
302
-	 * @param int $position
303
-	 * @return string encrypted data
304
-	 */
305
-	public function encrypt($data, $position = 0) {
306
-		// If extra data is left over from the last round, make sure it
307
-		// is integrated into the next block
308
-		if ($this->writeCache) {
309
-
310
-			// Concat writeCache to start of $data
311
-			$data = $this->writeCache . $data;
312
-
313
-			// Clear the write cache, ready for reuse - it has been
314
-			// flushed and its old contents processed
315
-			$this->writeCache = '';
316
-		}
317
-
318
-		$encrypted = '';
319
-		// While there still remains some data to be processed & written
320
-		while (strlen($data) > 0) {
321
-
322
-			// Remaining length for this iteration, not of the
323
-			// entire file (may be greater than 8192 bytes)
324
-			$remainingLength = strlen($data);
325
-
326
-			// If data remaining to be written is less than the
327
-			// size of 1 6126 byte block
328
-			if ($remainingLength < $this->unencryptedBlockSizeSigned) {
329
-
330
-				// Set writeCache to contents of $data
331
-				// The writeCache will be carried over to the
332
-				// next write round, and added to the start of
333
-				// $data to ensure that written blocks are
334
-				// always the correct length. If there is still
335
-				// data in writeCache after the writing round
336
-				// has finished, then the data will be written
337
-				// to disk by $this->flush().
338
-				$this->writeCache = $data;
339
-
340
-				// Clear $data ready for next round
341
-				$data = '';
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
-		return $encrypted;
357
-	}
358
-
359
-	/**
360
-	 * decrypt data
361
-	 *
362
-	 * @param string $data you want to decrypt
363
-	 * @param int|string $position
364
-	 * @return string decrypted data
365
-	 * @throws DecryptionFailedException
366
-	 */
367
-	public function decrypt($data, $position = 0) {
368
-		if (empty($this->fileKey)) {
369
-			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
370
-			$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.');
371
-			$this->logger->error($msg);
372
-
373
-			throw new DecryptionFailedException($msg, $hint);
374
-		}
375
-
376
-		return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
377
-	}
378
-
379
-	/**
380
-	 * update encrypted file, e.g. give additional users access to the file
381
-	 *
382
-	 * @param string $path path to the file which should be updated
383
-	 * @param string $uid of the user who performs the operation
384
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
385
-	 * @return boolean
386
-	 */
387
-	public function update($path, $uid, array $accessList) {
388
-		if (empty($accessList)) {
389
-			if (isset(self::$rememberVersion[$path])) {
390
-				$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
391
-				unset(self::$rememberVersion[$path]);
392
-			}
393
-			return;
394
-		}
395
-
396
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
397
-
398
-		if (!empty($fileKey)) {
399
-			$publicKeys = [];
400
-			if ($this->useMasterPassword === true) {
401
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
402
-			} else {
403
-				foreach ($accessList['users'] as $user) {
404
-					try {
405
-						$publicKeys[$user] = $this->keyManager->getPublicKey($user);
406
-					} catch (PublicKeyMissingException $e) {
407
-						$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
408
-					}
409
-				}
410
-			}
411
-
412
-			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
413
-
414
-			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
415
-
416
-			$this->keyManager->deleteAllFileKeys($path);
417
-
418
-			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
419
-		} else {
420
-			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
421
-				['file' => $path, 'app' => 'encryption']);
422
-
423
-			return false;
424
-		}
425
-
426
-		return true;
427
-	}
428
-
429
-	/**
430
-	 * should the file be encrypted or not
431
-	 *
432
-	 * @param string $path
433
-	 * @return boolean
434
-	 */
435
-	public function shouldEncrypt($path) {
436
-		if ($this->util->shouldEncryptHomeStorage() === false) {
437
-			$storage = $this->util->getStorage($path);
438
-			if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
439
-				return false;
440
-			}
441
-		}
442
-		$parts = explode('/', $path);
443
-		if (count($parts) < 4) {
444
-			return false;
445
-		}
446
-
447
-		if ($parts[2] === 'files') {
448
-			return true;
449
-		}
450
-		if ($parts[2] === 'files_versions') {
451
-			return true;
452
-		}
453
-		if ($parts[2] === 'files_trashbin') {
454
-			return true;
455
-		}
456
-
457
-		return false;
458
-	}
459
-
460
-	/**
461
-	 * get size of the unencrypted payload per block.
462
-	 * Nextcloud read/write files with a block size of 8192 byte
463
-	 *
464
-	 * @param bool $signed
465
-	 * @return int
466
-	 */
467
-	public function getUnencryptedBlockSize($signed = false) {
468
-		if ($signed === false) {
469
-			return $this->unencryptedBlockSize;
470
-		}
471
-
472
-		return $this->unencryptedBlockSizeSigned;
473
-	}
474
-
475
-	/**
476
-	 * check if the encryption module is able to read the file,
477
-	 * e.g. if all encryption keys exists
478
-	 *
479
-	 * @param string $path
480
-	 * @param string $uid user for whom we want to check if he can read the file
481
-	 * @return bool
482
-	 * @throws DecryptionFailedException
483
-	 */
484
-	public function isReadable($path, $uid) {
485
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
486
-		if (empty($fileKey)) {
487
-			$owner = $this->util->getOwner($path);
488
-			if ($owner !== $uid) {
489
-				// if it is a shared file we throw a exception with a useful
490
-				// error message because in this case it means that the file was
491
-				// shared with the user at a point where the user didn't had a
492
-				// valid private/public key
493
-				$msg = 'Encryption module "' . $this->getDisplayName() .
494
-					'" is not able to read ' . $path;
495
-				$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.');
496
-				$this->logger->warning($msg);
497
-				throw new DecryptionFailedException($msg, $hint);
498
-			}
499
-			return false;
500
-		}
501
-
502
-		return true;
503
-	}
504
-
505
-	/**
506
-	 * Initial encryption of all files
507
-	 *
508
-	 * @param InputInterface $input
509
-	 * @param OutputInterface $output write some status information to the terminal during encryption
510
-	 */
511
-	public function encryptAll(InputInterface $input, OutputInterface $output) {
512
-		$this->encryptAll->encryptAll($input, $output);
513
-	}
514
-
515
-	/**
516
-	 * prepare module to perform decrypt all operation
517
-	 *
518
-	 * @param InputInterface $input
519
-	 * @param OutputInterface $output
520
-	 * @param string $user
521
-	 * @return bool
522
-	 */
523
-	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
524
-		return $this->decryptAll->prepare($input, $output, $user);
525
-	}
526
-
527
-
528
-	/**
529
-	 * @param string $path
530
-	 * @return string
531
-	 */
532
-	protected function getPathToRealFile($path) {
533
-		$realPath = $path;
534
-		$parts = explode('/', $path);
535
-		if ($parts[2] === 'files_versions') {
536
-			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
537
-			$length = strrpos($realPath, '.');
538
-			$realPath = substr($realPath, 0, $length);
539
-		}
540
-
541
-		return $realPath;
542
-	}
543
-
544
-	/**
545
-	 * remove .part file extension and the ocTransferId from the file to get the
546
-	 * original file name
547
-	 *
548
-	 * @param string $path
549
-	 * @return string
550
-	 */
551
-	protected function stripPartFileExtension($path) {
552
-		if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
553
-			$pos = strrpos($path, '.', -6);
554
-			$path = substr($path, 0, $pos);
555
-		}
556
-
557
-		return $path;
558
-	}
559
-
560
-	/**
561
-	 * get owner of a file
562
-	 *
563
-	 * @param string $path
564
-	 * @return string
565
-	 */
566
-	protected function getOwner($path) {
567
-		if (!isset($this->owner[$path])) {
568
-			$this->owner[$path] = $this->util->getOwner($path);
569
-		}
570
-		return $this->owner[$path];
571
-	}
572
-
573
-	/**
574
-	 * Check if the module is ready to be used by that specific user.
575
-	 * In case a module is not ready - because e.g. key pairs have not been generated
576
-	 * upon login this method can return false before any operation starts and might
577
-	 * cause issues during operations.
578
-	 *
579
-	 * @param string $user
580
-	 * @return boolean
581
-	 * @since 9.1.0
582
-	 */
583
-	public function isReadyForUser($user) {
584
-		if ($this->util->isMasterKeyEnabled()) {
585
-			return true;
586
-		}
587
-		return $this->keyManager->userHasKeys($user);
588
-	}
589
-
590
-	/**
591
-	 * We only need a detailed access list if the master key is not enabled
592
-	 *
593
-	 * @return bool
594
-	 */
595
-	public function needDetailedAccessList() {
596
-		return !$this->util->isMasterKeyEnabled();
597
-	}
49
+    public const ID = 'OC_DEFAULT_MODULE';
50
+    public const DISPLAY_NAME = 'Default encryption module';
51
+
52
+    /**
53
+     * @var Crypt
54
+     */
55
+    private $crypt;
56
+
57
+    /** @var string */
58
+    private $cipher;
59
+
60
+    /** @var string */
61
+    private $path;
62
+
63
+    /** @var string */
64
+    private $user;
65
+
66
+    /** @var  array */
67
+    private $owner;
68
+
69
+    /** @var string */
70
+    private $fileKey;
71
+
72
+    /** @var string */
73
+    private $writeCache;
74
+
75
+    /** @var KeyManager */
76
+    private $keyManager;
77
+
78
+    /** @var array */
79
+    private $accessList;
80
+
81
+    /** @var boolean */
82
+    private $isWriteOperation;
83
+
84
+    /** @var Util */
85
+    private $util;
86
+
87
+    /** @var  Session */
88
+    private $session;
89
+
90
+    /** @var  ILogger */
91
+    private $logger;
92
+
93
+    /** @var IL10N */
94
+    private $l;
95
+
96
+    /** @var EncryptAll */
97
+    private $encryptAll;
98
+
99
+    /** @var  bool */
100
+    private $useMasterPassword;
101
+
102
+    /** @var DecryptAll  */
103
+    private $decryptAll;
104
+
105
+    /** @var int unencrypted block size if block contains signature */
106
+    private $unencryptedBlockSizeSigned = 6072;
107
+
108
+    /** @var int unencrypted block size */
109
+    private $unencryptedBlockSize = 6126;
110
+
111
+    /** @var int Current version of the file */
112
+    private $version = 0;
113
+
114
+    /** @var array remember encryption signature version */
115
+    private static $rememberVersion = [];
116
+
117
+
118
+    /**
119
+     *
120
+     * @param Crypt $crypt
121
+     * @param KeyManager $keyManager
122
+     * @param Util $util
123
+     * @param Session $session
124
+     * @param EncryptAll $encryptAll
125
+     * @param DecryptAll $decryptAll
126
+     * @param ILogger $logger
127
+     * @param IL10N $il10n
128
+     */
129
+    public function __construct(Crypt $crypt,
130
+                                KeyManager $keyManager,
131
+                                Util $util,
132
+                                Session $session,
133
+                                EncryptAll $encryptAll,
134
+                                DecryptAll $decryptAll,
135
+                                ILogger $logger,
136
+                                IL10N $il10n) {
137
+        $this->crypt = $crypt;
138
+        $this->keyManager = $keyManager;
139
+        $this->util = $util;
140
+        $this->session = $session;
141
+        $this->encryptAll = $encryptAll;
142
+        $this->decryptAll = $decryptAll;
143
+        $this->logger = $logger;
144
+        $this->l = $il10n;
145
+        $this->owner = [];
146
+        $this->useMasterPassword = $util->isMasterKeyEnabled();
147
+    }
148
+
149
+    /**
150
+     * @return string defining the technical unique id
151
+     */
152
+    public function getId() {
153
+        return self::ID;
154
+    }
155
+
156
+    /**
157
+     * In comparison to getKey() this function returns a human readable (maybe translated) name
158
+     *
159
+     * @return string
160
+     */
161
+    public function getDisplayName() {
162
+        return self::DISPLAY_NAME;
163
+    }
164
+
165
+    /**
166
+     * start receiving chunks from a file. This is the place where you can
167
+     * perform some initial step before starting encrypting/decrypting the
168
+     * chunks
169
+     *
170
+     * @param string $path to the file
171
+     * @param string $user who read/write the file
172
+     * @param string $mode php stream open mode
173
+     * @param array $header contains the header data read from the file
174
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
175
+     *
176
+     * @return array $header contain data as key-value pairs which should be
177
+     *                       written to the header, in case of a write operation
178
+     *                       or if no additional data is needed return a empty array
179
+     */
180
+    public function begin($path, $user, $mode, array $header, array $accessList) {
181
+        $this->path = $this->getPathToRealFile($path);
182
+        $this->accessList = $accessList;
183
+        $this->user = $user;
184
+        $this->isWriteOperation = false;
185
+        $this->writeCache = '';
186
+
187
+        if ($this->session->isReady() === false) {
188
+            // if the master key is enabled we can initialize encryption
189
+            // with a empty password and user name
190
+            if ($this->util->isMasterKeyEnabled()) {
191
+                $this->keyManager->init('', '');
192
+            }
193
+        }
194
+
195
+        if ($this->session->decryptAllModeActivated()) {
196
+            $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
197
+            $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
198
+            $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
199
+                $shareKey,
200
+                $this->session->getDecryptAllKey());
201
+        } else {
202
+            $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
203
+        }
204
+
205
+        // always use the version from the original file, also part files
206
+        // need to have a correct version number if they get moved over to the
207
+        // final location
208
+        $this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
209
+
210
+        if (
211
+            $mode === 'w'
212
+            || $mode === 'w+'
213
+            || $mode === 'wb'
214
+            || $mode === 'wb+'
215
+        ) {
216
+            $this->isWriteOperation = true;
217
+            if (empty($this->fileKey)) {
218
+                $this->fileKey = $this->crypt->generateFileKey();
219
+            }
220
+        } else {
221
+            // if we read a part file we need to increase the version by 1
222
+            // because the version number was also increased by writing
223
+            // the part file
224
+            if (Scanner::isPartialFile($path)) {
225
+                $this->version = $this->version + 1;
226
+            }
227
+        }
228
+
229
+        if ($this->isWriteOperation) {
230
+            $this->cipher = $this->crypt->getCipher();
231
+        } elseif (isset($header['cipher'])) {
232
+            $this->cipher = $header['cipher'];
233
+        } else {
234
+            // if we read a file without a header we fall-back to the legacy cipher
235
+            // which was used in <=oC6
236
+            $this->cipher = $this->crypt->getLegacyCipher();
237
+        }
238
+
239
+        return ['cipher' => $this->cipher, 'signed' => 'true'];
240
+    }
241
+
242
+    /**
243
+     * last chunk received. This is the place where you can perform some final
244
+     * operation and return some remaining data if something is left in your
245
+     * buffer.
246
+     *
247
+     * @param string $path to the file
248
+     * @param int $position
249
+     * @return string remained data which should be written to the file in case
250
+     *                of a write operation
251
+     * @throws PublicKeyMissingException
252
+     * @throws \Exception
253
+     * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
254
+     */
255
+    public function end($path, $position = 0) {
256
+        $result = '';
257
+        if ($this->isWriteOperation) {
258
+            // in case of a part file we remember the new signature versions
259
+            // the version will be set later on update.
260
+            // This way we make sure that other apps listening to the pre-hooks
261
+            // still get the old version which should be the correct value for them
262
+            if (Scanner::isPartialFile($path)) {
263
+                self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
264
+            }
265
+            if (!empty($this->writeCache)) {
266
+                $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
267
+                $this->writeCache = '';
268
+            }
269
+            $publicKeys = [];
270
+            if ($this->useMasterPassword === true) {
271
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
272
+            } else {
273
+                foreach ($this->accessList['users'] as $uid) {
274
+                    try {
275
+                        $publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
276
+                    } catch (PublicKeyMissingException $e) {
277
+                        $this->logger->warning(
278
+                            'no public key found for user "{uid}", user will not be able to read the file',
279
+                            ['app' => 'encryption', 'uid' => $uid]
280
+                        );
281
+                        // if the public key of the owner is missing we should fail
282
+                        if ($uid === $this->user) {
283
+                            throw $e;
284
+                        }
285
+                    }
286
+                }
287
+            }
288
+
289
+            $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path));
290
+            $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
291
+            $this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
292
+        }
293
+        return $result;
294
+    }
295
+
296
+
297
+
298
+    /**
299
+     * encrypt data
300
+     *
301
+     * @param string $data you want to encrypt
302
+     * @param int $position
303
+     * @return string encrypted data
304
+     */
305
+    public function encrypt($data, $position = 0) {
306
+        // If extra data is left over from the last round, make sure it
307
+        // is integrated into the next block
308
+        if ($this->writeCache) {
309
+
310
+            // Concat writeCache to start of $data
311
+            $data = $this->writeCache . $data;
312
+
313
+            // Clear the write cache, ready for reuse - it has been
314
+            // flushed and its old contents processed
315
+            $this->writeCache = '';
316
+        }
317
+
318
+        $encrypted = '';
319
+        // While there still remains some data to be processed & written
320
+        while (strlen($data) > 0) {
321
+
322
+            // Remaining length for this iteration, not of the
323
+            // entire file (may be greater than 8192 bytes)
324
+            $remainingLength = strlen($data);
325
+
326
+            // If data remaining to be written is less than the
327
+            // size of 1 6126 byte block
328
+            if ($remainingLength < $this->unencryptedBlockSizeSigned) {
329
+
330
+                // Set writeCache to contents of $data
331
+                // The writeCache will be carried over to the
332
+                // next write round, and added to the start of
333
+                // $data to ensure that written blocks are
334
+                // always the correct length. If there is still
335
+                // data in writeCache after the writing round
336
+                // has finished, then the data will be written
337
+                // to disk by $this->flush().
338
+                $this->writeCache = $data;
339
+
340
+                // Clear $data ready for next round
341
+                $data = '';
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
+        return $encrypted;
357
+    }
358
+
359
+    /**
360
+     * decrypt data
361
+     *
362
+     * @param string $data you want to decrypt
363
+     * @param int|string $position
364
+     * @return string decrypted data
365
+     * @throws DecryptionFailedException
366
+     */
367
+    public function decrypt($data, $position = 0) {
368
+        if (empty($this->fileKey)) {
369
+            $msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
370
+            $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.');
371
+            $this->logger->error($msg);
372
+
373
+            throw new DecryptionFailedException($msg, $hint);
374
+        }
375
+
376
+        return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
377
+    }
378
+
379
+    /**
380
+     * update encrypted file, e.g. give additional users access to the file
381
+     *
382
+     * @param string $path path to the file which should be updated
383
+     * @param string $uid of the user who performs the operation
384
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
385
+     * @return boolean
386
+     */
387
+    public function update($path, $uid, array $accessList) {
388
+        if (empty($accessList)) {
389
+            if (isset(self::$rememberVersion[$path])) {
390
+                $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
391
+                unset(self::$rememberVersion[$path]);
392
+            }
393
+            return;
394
+        }
395
+
396
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
397
+
398
+        if (!empty($fileKey)) {
399
+            $publicKeys = [];
400
+            if ($this->useMasterPassword === true) {
401
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
402
+            } else {
403
+                foreach ($accessList['users'] as $user) {
404
+                    try {
405
+                        $publicKeys[$user] = $this->keyManager->getPublicKey($user);
406
+                    } catch (PublicKeyMissingException $e) {
407
+                        $this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
408
+                    }
409
+                }
410
+            }
411
+
412
+            $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
413
+
414
+            $encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
415
+
416
+            $this->keyManager->deleteAllFileKeys($path);
417
+
418
+            $this->keyManager->setAllFileKeys($path, $encryptedFileKey);
419
+        } else {
420
+            $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
421
+                ['file' => $path, 'app' => 'encryption']);
422
+
423
+            return false;
424
+        }
425
+
426
+        return true;
427
+    }
428
+
429
+    /**
430
+     * should the file be encrypted or not
431
+     *
432
+     * @param string $path
433
+     * @return boolean
434
+     */
435
+    public function shouldEncrypt($path) {
436
+        if ($this->util->shouldEncryptHomeStorage() === false) {
437
+            $storage = $this->util->getStorage($path);
438
+            if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
439
+                return false;
440
+            }
441
+        }
442
+        $parts = explode('/', $path);
443
+        if (count($parts) < 4) {
444
+            return false;
445
+        }
446
+
447
+        if ($parts[2] === 'files') {
448
+            return true;
449
+        }
450
+        if ($parts[2] === 'files_versions') {
451
+            return true;
452
+        }
453
+        if ($parts[2] === 'files_trashbin') {
454
+            return true;
455
+        }
456
+
457
+        return false;
458
+    }
459
+
460
+    /**
461
+     * get size of the unencrypted payload per block.
462
+     * Nextcloud read/write files with a block size of 8192 byte
463
+     *
464
+     * @param bool $signed
465
+     * @return int
466
+     */
467
+    public function getUnencryptedBlockSize($signed = false) {
468
+        if ($signed === false) {
469
+            return $this->unencryptedBlockSize;
470
+        }
471
+
472
+        return $this->unencryptedBlockSizeSigned;
473
+    }
474
+
475
+    /**
476
+     * check if the encryption module is able to read the file,
477
+     * e.g. if all encryption keys exists
478
+     *
479
+     * @param string $path
480
+     * @param string $uid user for whom we want to check if he can read the file
481
+     * @return bool
482
+     * @throws DecryptionFailedException
483
+     */
484
+    public function isReadable($path, $uid) {
485
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
486
+        if (empty($fileKey)) {
487
+            $owner = $this->util->getOwner($path);
488
+            if ($owner !== $uid) {
489
+                // if it is a shared file we throw a exception with a useful
490
+                // error message because in this case it means that the file was
491
+                // shared with the user at a point where the user didn't had a
492
+                // valid private/public key
493
+                $msg = 'Encryption module "' . $this->getDisplayName() .
494
+                    '" is not able to read ' . $path;
495
+                $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.');
496
+                $this->logger->warning($msg);
497
+                throw new DecryptionFailedException($msg, $hint);
498
+            }
499
+            return false;
500
+        }
501
+
502
+        return true;
503
+    }
504
+
505
+    /**
506
+     * Initial encryption of all files
507
+     *
508
+     * @param InputInterface $input
509
+     * @param OutputInterface $output write some status information to the terminal during encryption
510
+     */
511
+    public function encryptAll(InputInterface $input, OutputInterface $output) {
512
+        $this->encryptAll->encryptAll($input, $output);
513
+    }
514
+
515
+    /**
516
+     * prepare module to perform decrypt all operation
517
+     *
518
+     * @param InputInterface $input
519
+     * @param OutputInterface $output
520
+     * @param string $user
521
+     * @return bool
522
+     */
523
+    public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
524
+        return $this->decryptAll->prepare($input, $output, $user);
525
+    }
526
+
527
+
528
+    /**
529
+     * @param string $path
530
+     * @return string
531
+     */
532
+    protected function getPathToRealFile($path) {
533
+        $realPath = $path;
534
+        $parts = explode('/', $path);
535
+        if ($parts[2] === 'files_versions') {
536
+            $realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
537
+            $length = strrpos($realPath, '.');
538
+            $realPath = substr($realPath, 0, $length);
539
+        }
540
+
541
+        return $realPath;
542
+    }
543
+
544
+    /**
545
+     * remove .part file extension and the ocTransferId from the file to get the
546
+     * original file name
547
+     *
548
+     * @param string $path
549
+     * @return string
550
+     */
551
+    protected function stripPartFileExtension($path) {
552
+        if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
553
+            $pos = strrpos($path, '.', -6);
554
+            $path = substr($path, 0, $pos);
555
+        }
556
+
557
+        return $path;
558
+    }
559
+
560
+    /**
561
+     * get owner of a file
562
+     *
563
+     * @param string $path
564
+     * @return string
565
+     */
566
+    protected function getOwner($path) {
567
+        if (!isset($this->owner[$path])) {
568
+            $this->owner[$path] = $this->util->getOwner($path);
569
+        }
570
+        return $this->owner[$path];
571
+    }
572
+
573
+    /**
574
+     * Check if the module is ready to be used by that specific user.
575
+     * In case a module is not ready - because e.g. key pairs have not been generated
576
+     * upon login this method can return false before any operation starts and might
577
+     * cause issues during operations.
578
+     *
579
+     * @param string $user
580
+     * @return boolean
581
+     * @since 9.1.0
582
+     */
583
+    public function isReadyForUser($user) {
584
+        if ($this->util->isMasterKeyEnabled()) {
585
+            return true;
586
+        }
587
+        return $this->keyManager->userHasKeys($user);
588
+    }
589
+
590
+    /**
591
+     * We only need a detailed access list if the master key is not enabled
592
+     *
593
+     * @return bool
594
+     */
595
+    public function needDetailedAccessList() {
596
+        return !$this->util->isMasterKeyEnabled();
597
+    }
598 598
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +661 added lines, -661 removed lines patch added patch discarded remove patch
@@ -57,665 +57,665 @@
 block discarded – undo
57 57
  * @package OCA\Encryption\Crypto
58 58
  */
59 59
 class Crypt {
60
-	public const DEFAULT_CIPHER = 'AES-256-CTR';
61
-	// default cipher from old Nextcloud versions
62
-	public const LEGACY_CIPHER = 'AES-128-CFB';
63
-
64
-	// default key format, old Nextcloud version encrypted the private key directly
65
-	// with the user password
66
-	public const LEGACY_KEY_FORMAT = 'password';
67
-
68
-	public const HEADER_START = 'HBEGIN';
69
-	public const HEADER_END = 'HEND';
70
-
71
-	/** @var ILogger */
72
-	private $logger;
73
-
74
-	/** @var string */
75
-	private $user;
76
-
77
-	/** @var IConfig */
78
-	private $config;
79
-
80
-	/** @var array */
81
-	private $supportedKeyFormats;
82
-
83
-	/** @var IL10N */
84
-	private $l;
85
-
86
-	/** @var array */
87
-	private $supportedCiphersAndKeySize = [
88
-		'AES-256-CTR' => 32,
89
-		'AES-128-CTR' => 16,
90
-		'AES-256-CFB' => 32,
91
-		'AES-128-CFB' => 16,
92
-	];
93
-
94
-	/** @var bool */
95
-	private $supportLegacy;
96
-
97
-	/**
98
-	 * @param ILogger $logger
99
-	 * @param IUserSession $userSession
100
-	 * @param IConfig $config
101
-	 * @param IL10N $l
102
-	 */
103
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
104
-		$this->logger = $logger;
105
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
106
-		$this->config = $config;
107
-		$this->l = $l;
108
-		$this->supportedKeyFormats = ['hash', 'password'];
109
-
110
-		$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
111
-	}
112
-
113
-	/**
114
-	 * create new private/public key-pair for user
115
-	 *
116
-	 * @return array|bool
117
-	 */
118
-	public function createKeyPair() {
119
-		$log = $this->logger;
120
-		$res = $this->getOpenSSLPKey();
121
-
122
-		if (!$res) {
123
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
124
-				['app' => 'encryption']);
125
-
126
-			if (openssl_error_string()) {
127
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
128
-					['app' => 'encryption']);
129
-			}
130
-		} elseif (openssl_pkey_export($res,
131
-			$privateKey,
132
-			null,
133
-			$this->getOpenSSLConfig())) {
134
-			$keyDetails = openssl_pkey_get_details($res);
135
-			$publicKey = $keyDetails['key'];
136
-
137
-			return [
138
-				'publicKey' => $publicKey,
139
-				'privateKey' => $privateKey
140
-			];
141
-		}
142
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
143
-			['app' => 'encryption']);
144
-		if (openssl_error_string()) {
145
-			$log->error('Encryption Library:' . openssl_error_string(),
146
-				['app' => 'encryption']);
147
-		}
148
-
149
-		return false;
150
-	}
151
-
152
-	/**
153
-	 * Generates a new private key
154
-	 *
155
-	 * @return resource
156
-	 */
157
-	public function getOpenSSLPKey() {
158
-		$config = $this->getOpenSSLConfig();
159
-		return openssl_pkey_new($config);
160
-	}
161
-
162
-	/**
163
-	 * get openSSL Config
164
-	 *
165
-	 * @return array
166
-	 */
167
-	private function getOpenSSLConfig() {
168
-		$config = ['private_key_bits' => 4096];
169
-		$config = array_merge(
170
-			$config,
171
-			$this->config->getSystemValue('openssl', [])
172
-		);
173
-		return $config;
174
-	}
175
-
176
-	/**
177
-	 * @param string $plainContent
178
-	 * @param string $passPhrase
179
-	 * @param int $version
180
-	 * @param int $position
181
-	 * @return false|string
182
-	 * @throws EncryptionFailedException
183
-	 */
184
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
185
-		if (!$plainContent) {
186
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
187
-				['app' => 'encryption']);
188
-			return false;
189
-		}
190
-
191
-		$iv = $this->generateIv();
192
-
193
-		$encryptedContent = $this->encrypt($plainContent,
194
-			$iv,
195
-			$passPhrase,
196
-			$this->getCipher());
197
-
198
-		// Create a signature based on the key as well as the current version
199
-		$sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
200
-
201
-		// combine content to encrypt the IV identifier and actual IV
202
-		$catFile = $this->concatIV($encryptedContent, $iv);
203
-		$catFile = $this->concatSig($catFile, $sig);
204
-		return $this->addPadding($catFile);
205
-	}
206
-
207
-	/**
208
-	 * generate header for encrypted file
209
-	 *
210
-	 * @param string $keyFormat (can be 'hash' or 'password')
211
-	 * @return string
212
-	 * @throws \InvalidArgumentException
213
-	 */
214
-	public function generateHeader($keyFormat = 'hash') {
215
-		if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
216
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
217
-		}
218
-
219
-		$cipher = $this->getCipher();
220
-
221
-		$header = self::HEADER_START
222
-			. ':cipher:' . $cipher
223
-			. ':keyFormat:' . $keyFormat
224
-			. ':' . self::HEADER_END;
225
-
226
-		return $header;
227
-	}
228
-
229
-	/**
230
-	 * @param string $plainContent
231
-	 * @param string $iv
232
-	 * @param string $passPhrase
233
-	 * @param string $cipher
234
-	 * @return string
235
-	 * @throws EncryptionFailedException
236
-	 */
237
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
238
-		$encryptedContent = openssl_encrypt($plainContent,
239
-			$cipher,
240
-			$passPhrase,
241
-			false,
242
-			$iv);
243
-
244
-		if (!$encryptedContent) {
245
-			$error = 'Encryption (symmetric) of content failed';
246
-			$this->logger->error($error . openssl_error_string(),
247
-				['app' => 'encryption']);
248
-			throw new EncryptionFailedException($error);
249
-		}
250
-
251
-		return $encryptedContent;
252
-	}
253
-
254
-	/**
255
-	 * return Cipher either from config.php or the default cipher defined in
256
-	 * this class
257
-	 *
258
-	 * @return string
259
-	 */
260
-	public function getCipher() {
261
-		$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
262
-		if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
263
-			$this->logger->warning(
264
-					sprintf(
265
-							'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
266
-							$cipher,
267
-							self::DEFAULT_CIPHER
268
-					),
269
-				['app' => 'encryption']);
270
-			$cipher = self::DEFAULT_CIPHER;
271
-		}
272
-
273
-		// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
274
-		if (OPENSSL_VERSION_NUMBER < 0x1000101f) {
275
-			if ($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
276
-				$cipher = self::LEGACY_CIPHER;
277
-			}
278
-		}
279
-
280
-		return $cipher;
281
-	}
282
-
283
-	/**
284
-	 * get key size depending on the cipher
285
-	 *
286
-	 * @param string $cipher
287
-	 * @return int
288
-	 * @throws \InvalidArgumentException
289
-	 */
290
-	protected function getKeySize($cipher) {
291
-		if (isset($this->supportedCiphersAndKeySize[$cipher])) {
292
-			return $this->supportedCiphersAndKeySize[$cipher];
293
-		}
294
-
295
-		throw new \InvalidArgumentException(
296
-			sprintf(
297
-					'Unsupported cipher (%s) defined.',
298
-					$cipher
299
-			)
300
-		);
301
-	}
302
-
303
-	/**
304
-	 * get legacy cipher
305
-	 *
306
-	 * @return string
307
-	 */
308
-	public function getLegacyCipher() {
309
-		if (!$this->supportLegacy) {
310
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
311
-		}
312
-
313
-		return self::LEGACY_CIPHER;
314
-	}
315
-
316
-	/**
317
-	 * @param string $encryptedContent
318
-	 * @param string $iv
319
-	 * @return string
320
-	 */
321
-	private function concatIV($encryptedContent, $iv) {
322
-		return $encryptedContent . '00iv00' . $iv;
323
-	}
324
-
325
-	/**
326
-	 * @param string $encryptedContent
327
-	 * @param string $signature
328
-	 * @return string
329
-	 */
330
-	private function concatSig($encryptedContent, $signature) {
331
-		return $encryptedContent . '00sig00' . $signature;
332
-	}
333
-
334
-	/**
335
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
336
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
337
-	 * encrypted content and is not used in any crypto primitive.
338
-	 *
339
-	 * @param string $data
340
-	 * @return string
341
-	 */
342
-	private function addPadding($data) {
343
-		return $data . 'xxx';
344
-	}
345
-
346
-	/**
347
-	 * generate password hash used to encrypt the users private key
348
-	 *
349
-	 * @param string $password
350
-	 * @param string $cipher
351
-	 * @param string $uid only used for user keys
352
-	 * @return string
353
-	 */
354
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
355
-		$instanceId = $this->config->getSystemValue('instanceid');
356
-		$instanceSecret = $this->config->getSystemValue('secret');
357
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
358
-		$keySize = $this->getKeySize($cipher);
359
-
360
-		$hash = hash_pbkdf2(
361
-			'sha256',
362
-			$password,
363
-			$salt,
364
-			100000,
365
-			$keySize,
366
-			true
367
-		);
368
-
369
-		return $hash;
370
-	}
371
-
372
-	/**
373
-	 * encrypt private key
374
-	 *
375
-	 * @param string $privateKey
376
-	 * @param string $password
377
-	 * @param string $uid for regular users, empty for system keys
378
-	 * @return false|string
379
-	 */
380
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
381
-		$cipher = $this->getCipher();
382
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
383
-		$encryptedKey = $this->symmetricEncryptFileContent(
384
-			$privateKey,
385
-			$hash,
386
-			0,
387
-			0
388
-		);
389
-
390
-		return $encryptedKey;
391
-	}
392
-
393
-	/**
394
-	 * @param string $privateKey
395
-	 * @param string $password
396
-	 * @param string $uid for regular users, empty for system keys
397
-	 * @return false|string
398
-	 */
399
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
400
-		$header = $this->parseHeader($privateKey);
401
-
402
-		if (isset($header['cipher'])) {
403
-			$cipher = $header['cipher'];
404
-		} else {
405
-			$cipher = $this->getLegacyCipher();
406
-		}
407
-
408
-		if (isset($header['keyFormat'])) {
409
-			$keyFormat = $header['keyFormat'];
410
-		} else {
411
-			$keyFormat = self::LEGACY_KEY_FORMAT;
412
-		}
413
-
414
-		if ($keyFormat === 'hash') {
415
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
416
-		}
417
-
418
-		// If we found a header we need to remove it from the key we want to decrypt
419
-		if (!empty($header)) {
420
-			$privateKey = substr($privateKey,
421
-				strpos($privateKey,
422
-					self::HEADER_END) + strlen(self::HEADER_END));
423
-		}
424
-
425
-		$plainKey = $this->symmetricDecryptFileContent(
426
-			$privateKey,
427
-			$password,
428
-			$cipher,
429
-			0
430
-		);
431
-
432
-		if ($this->isValidPrivateKey($plainKey) === false) {
433
-			return false;
434
-		}
435
-
436
-		return $plainKey;
437
-	}
438
-
439
-	/**
440
-	 * check if it is a valid private key
441
-	 *
442
-	 * @param string $plainKey
443
-	 * @return bool
444
-	 */
445
-	protected function isValidPrivateKey($plainKey) {
446
-		$res = openssl_get_privatekey($plainKey);
447
-		if (is_resource($res)) {
448
-			$sslInfo = openssl_pkey_get_details($res);
449
-			if (isset($sslInfo['key'])) {
450
-				return true;
451
-			}
452
-		}
453
-
454
-		return false;
455
-	}
456
-
457
-	/**
458
-	 * @param string $keyFileContents
459
-	 * @param string $passPhrase
460
-	 * @param string $cipher
461
-	 * @param int $version
462
-	 * @param int|string $position
463
-	 * @return string
464
-	 * @throws DecryptionFailedException
465
-	 */
466
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
467
-		if ($keyFileContents == '') {
468
-			return '';
469
-		}
470
-
471
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
472
-
473
-		if ($catFile['signature'] !== false) {
474
-			try {
475
-				// First try the new format
476
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
477
-			} catch (GenericEncryptionException $e) {
478
-				// For compatibility with old files check the version without _
479
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
480
-			}
481
-		}
482
-
483
-		return $this->decrypt($catFile['encrypted'],
484
-			$catFile['iv'],
485
-			$passPhrase,
486
-			$cipher);
487
-	}
488
-
489
-	/**
490
-	 * check for valid signature
491
-	 *
492
-	 * @param string $data
493
-	 * @param string $passPhrase
494
-	 * @param string $expectedSignature
495
-	 * @throws GenericEncryptionException
496
-	 */
497
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
498
-		$enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
499
-
500
-		$signature = $this->createSignature($data, $passPhrase);
501
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
502
-
503
-		if (!$isCorrectHash && $enforceSignature) {
504
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
505
-		} elseif (!$isCorrectHash && !$enforceSignature) {
506
-			$this->logger->info("Signature check skipped", ['app' => 'encryption']);
507
-		}
508
-	}
509
-
510
-	/**
511
-	 * create signature
512
-	 *
513
-	 * @param string $data
514
-	 * @param string $passPhrase
515
-	 * @return string
516
-	 */
517
-	private function createSignature($data, $passPhrase) {
518
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
519
-		return hash_hmac('sha256', $data, $passPhrase);
520
-	}
521
-
522
-
523
-	/**
524
-	 * remove padding
525
-	 *
526
-	 * @param string $padded
527
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
528
-	 * @return string|false
529
-	 */
530
-	private function removePadding($padded, $hasSignature = false) {
531
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
532
-			return substr($padded, 0, -2);
533
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
534
-			return substr($padded, 0, -3);
535
-		}
536
-		return false;
537
-	}
538
-
539
-	/**
540
-	 * split meta data from encrypted file
541
-	 * Note: for now, we assume that the meta data always start with the iv
542
-	 *       followed by the signature, if available
543
-	 *
544
-	 * @param string $catFile
545
-	 * @param string $cipher
546
-	 * @return array
547
-	 */
548
-	private function splitMetaData($catFile, $cipher) {
549
-		if ($this->hasSignature($catFile, $cipher)) {
550
-			$catFile = $this->removePadding($catFile, true);
551
-			$meta = substr($catFile, -93);
552
-			$iv = substr($meta, strlen('00iv00'), 16);
553
-			$sig = substr($meta, 22 + strlen('00sig00'));
554
-			$encrypted = substr($catFile, 0, -93);
555
-		} else {
556
-			$catFile = $this->removePadding($catFile);
557
-			$meta = substr($catFile, -22);
558
-			$iv = substr($meta, -16);
559
-			$sig = false;
560
-			$encrypted = substr($catFile, 0, -22);
561
-		}
562
-
563
-		return [
564
-			'encrypted' => $encrypted,
565
-			'iv' => $iv,
566
-			'signature' => $sig
567
-		];
568
-	}
569
-
570
-	/**
571
-	 * check if encrypted block is signed
572
-	 *
573
-	 * @param string $catFile
574
-	 * @param string $cipher
575
-	 * @return bool
576
-	 * @throws GenericEncryptionException
577
-	 */
578
-	private function hasSignature($catFile, $cipher) {
579
-		$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
580
-
581
-		$meta = substr($catFile, -93);
582
-		$signaturePosition = strpos($meta, '00sig00');
583
-
584
-		// If we no longer support the legacy format then everything needs a signature
585
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
586
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
587
-		}
588
-
589
-		// enforce signature for the new 'CTR' ciphers
590
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
591
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
592
-		}
593
-
594
-		return ($signaturePosition !== false);
595
-	}
596
-
597
-
598
-	/**
599
-	 * @param string $encryptedContent
600
-	 * @param string $iv
601
-	 * @param string $passPhrase
602
-	 * @param string $cipher
603
-	 * @return string
604
-	 * @throws DecryptionFailedException
605
-	 */
606
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
607
-		$plainContent = openssl_decrypt($encryptedContent,
608
-			$cipher,
609
-			$passPhrase,
610
-			false,
611
-			$iv);
612
-
613
-		if ($plainContent) {
614
-			return $plainContent;
615
-		} else {
616
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
617
-		}
618
-	}
619
-
620
-	/**
621
-	 * @param string $data
622
-	 * @return array
623
-	 */
624
-	protected function parseHeader($data) {
625
-		$result = [];
626
-
627
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
628
-			$endAt = strpos($data, self::HEADER_END);
629
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
630
-
631
-			// +1 not to start with an ':' which would result in empty element at the beginning
632
-			$exploded = explode(':',
633
-				substr($header, strlen(self::HEADER_START) + 1));
634
-
635
-			$element = array_shift($exploded);
636
-
637
-			while ($element !== self::HEADER_END) {
638
-				$result[$element] = array_shift($exploded);
639
-				$element = array_shift($exploded);
640
-			}
641
-		}
642
-
643
-		return $result;
644
-	}
645
-
646
-	/**
647
-	 * generate initialization vector
648
-	 *
649
-	 * @return string
650
-	 * @throws GenericEncryptionException
651
-	 */
652
-	private function generateIv() {
653
-		return random_bytes(16);
654
-	}
655
-
656
-	/**
657
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
658
-	 * as file key
659
-	 *
660
-	 * @return string
661
-	 * @throws \Exception
662
-	 */
663
-	public function generateFileKey() {
664
-		return random_bytes(32);
665
-	}
666
-
667
-	/**
668
-	 * @param $encKeyFile
669
-	 * @param $shareKey
670
-	 * @param $privateKey
671
-	 * @return string
672
-	 * @throws MultiKeyDecryptException
673
-	 */
674
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
675
-		if (!$encKeyFile) {
676
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
677
-		}
678
-
679
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
680
-			return $plainContent;
681
-		} else {
682
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
683
-		}
684
-	}
685
-
686
-	/**
687
-	 * @param string $plainContent
688
-	 * @param array $keyFiles
689
-	 * @return array
690
-	 * @throws MultiKeyEncryptException
691
-	 */
692
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
693
-		// openssl_seal returns false without errors if plaincontent is empty
694
-		// so trigger our own error
695
-		if (empty($plainContent)) {
696
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
697
-		}
698
-
699
-		// Set empty vars to be set by openssl by reference
700
-		$sealed = '';
701
-		$shareKeys = [];
702
-		$mappedShareKeys = [];
703
-
704
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
705
-			$i = 0;
706
-
707
-			// Ensure each shareKey is labelled with its corresponding key id
708
-			foreach ($keyFiles as $userId => $publicKey) {
709
-				$mappedShareKeys[$userId] = $shareKeys[$i];
710
-				$i++;
711
-			}
712
-
713
-			return [
714
-				'keys' => $mappedShareKeys,
715
-				'data' => $sealed
716
-			];
717
-		} else {
718
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
719
-		}
720
-	}
60
+    public const DEFAULT_CIPHER = 'AES-256-CTR';
61
+    // default cipher from old Nextcloud versions
62
+    public const LEGACY_CIPHER = 'AES-128-CFB';
63
+
64
+    // default key format, old Nextcloud version encrypted the private key directly
65
+    // with the user password
66
+    public const LEGACY_KEY_FORMAT = 'password';
67
+
68
+    public const HEADER_START = 'HBEGIN';
69
+    public const HEADER_END = 'HEND';
70
+
71
+    /** @var ILogger */
72
+    private $logger;
73
+
74
+    /** @var string */
75
+    private $user;
76
+
77
+    /** @var IConfig */
78
+    private $config;
79
+
80
+    /** @var array */
81
+    private $supportedKeyFormats;
82
+
83
+    /** @var IL10N */
84
+    private $l;
85
+
86
+    /** @var array */
87
+    private $supportedCiphersAndKeySize = [
88
+        'AES-256-CTR' => 32,
89
+        'AES-128-CTR' => 16,
90
+        'AES-256-CFB' => 32,
91
+        'AES-128-CFB' => 16,
92
+    ];
93
+
94
+    /** @var bool */
95
+    private $supportLegacy;
96
+
97
+    /**
98
+     * @param ILogger $logger
99
+     * @param IUserSession $userSession
100
+     * @param IConfig $config
101
+     * @param IL10N $l
102
+     */
103
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
104
+        $this->logger = $logger;
105
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
106
+        $this->config = $config;
107
+        $this->l = $l;
108
+        $this->supportedKeyFormats = ['hash', 'password'];
109
+
110
+        $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
111
+    }
112
+
113
+    /**
114
+     * create new private/public key-pair for user
115
+     *
116
+     * @return array|bool
117
+     */
118
+    public function createKeyPair() {
119
+        $log = $this->logger;
120
+        $res = $this->getOpenSSLPKey();
121
+
122
+        if (!$res) {
123
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
124
+                ['app' => 'encryption']);
125
+
126
+            if (openssl_error_string()) {
127
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
128
+                    ['app' => 'encryption']);
129
+            }
130
+        } elseif (openssl_pkey_export($res,
131
+            $privateKey,
132
+            null,
133
+            $this->getOpenSSLConfig())) {
134
+            $keyDetails = openssl_pkey_get_details($res);
135
+            $publicKey = $keyDetails['key'];
136
+
137
+            return [
138
+                'publicKey' => $publicKey,
139
+                'privateKey' => $privateKey
140
+            ];
141
+        }
142
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
143
+            ['app' => 'encryption']);
144
+        if (openssl_error_string()) {
145
+            $log->error('Encryption Library:' . openssl_error_string(),
146
+                ['app' => 'encryption']);
147
+        }
148
+
149
+        return false;
150
+    }
151
+
152
+    /**
153
+     * Generates a new private key
154
+     *
155
+     * @return resource
156
+     */
157
+    public function getOpenSSLPKey() {
158
+        $config = $this->getOpenSSLConfig();
159
+        return openssl_pkey_new($config);
160
+    }
161
+
162
+    /**
163
+     * get openSSL Config
164
+     *
165
+     * @return array
166
+     */
167
+    private function getOpenSSLConfig() {
168
+        $config = ['private_key_bits' => 4096];
169
+        $config = array_merge(
170
+            $config,
171
+            $this->config->getSystemValue('openssl', [])
172
+        );
173
+        return $config;
174
+    }
175
+
176
+    /**
177
+     * @param string $plainContent
178
+     * @param string $passPhrase
179
+     * @param int $version
180
+     * @param int $position
181
+     * @return false|string
182
+     * @throws EncryptionFailedException
183
+     */
184
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
185
+        if (!$plainContent) {
186
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
187
+                ['app' => 'encryption']);
188
+            return false;
189
+        }
190
+
191
+        $iv = $this->generateIv();
192
+
193
+        $encryptedContent = $this->encrypt($plainContent,
194
+            $iv,
195
+            $passPhrase,
196
+            $this->getCipher());
197
+
198
+        // Create a signature based on the key as well as the current version
199
+        $sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
200
+
201
+        // combine content to encrypt the IV identifier and actual IV
202
+        $catFile = $this->concatIV($encryptedContent, $iv);
203
+        $catFile = $this->concatSig($catFile, $sig);
204
+        return $this->addPadding($catFile);
205
+    }
206
+
207
+    /**
208
+     * generate header for encrypted file
209
+     *
210
+     * @param string $keyFormat (can be 'hash' or 'password')
211
+     * @return string
212
+     * @throws \InvalidArgumentException
213
+     */
214
+    public function generateHeader($keyFormat = 'hash') {
215
+        if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
216
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
217
+        }
218
+
219
+        $cipher = $this->getCipher();
220
+
221
+        $header = self::HEADER_START
222
+            . ':cipher:' . $cipher
223
+            . ':keyFormat:' . $keyFormat
224
+            . ':' . self::HEADER_END;
225
+
226
+        return $header;
227
+    }
228
+
229
+    /**
230
+     * @param string $plainContent
231
+     * @param string $iv
232
+     * @param string $passPhrase
233
+     * @param string $cipher
234
+     * @return string
235
+     * @throws EncryptionFailedException
236
+     */
237
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
238
+        $encryptedContent = openssl_encrypt($plainContent,
239
+            $cipher,
240
+            $passPhrase,
241
+            false,
242
+            $iv);
243
+
244
+        if (!$encryptedContent) {
245
+            $error = 'Encryption (symmetric) of content failed';
246
+            $this->logger->error($error . openssl_error_string(),
247
+                ['app' => 'encryption']);
248
+            throw new EncryptionFailedException($error);
249
+        }
250
+
251
+        return $encryptedContent;
252
+    }
253
+
254
+    /**
255
+     * return Cipher either from config.php or the default cipher defined in
256
+     * this class
257
+     *
258
+     * @return string
259
+     */
260
+    public function getCipher() {
261
+        $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
262
+        if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
263
+            $this->logger->warning(
264
+                    sprintf(
265
+                            'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
266
+                            $cipher,
267
+                            self::DEFAULT_CIPHER
268
+                    ),
269
+                ['app' => 'encryption']);
270
+            $cipher = self::DEFAULT_CIPHER;
271
+        }
272
+
273
+        // Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
274
+        if (OPENSSL_VERSION_NUMBER < 0x1000101f) {
275
+            if ($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
276
+                $cipher = self::LEGACY_CIPHER;
277
+            }
278
+        }
279
+
280
+        return $cipher;
281
+    }
282
+
283
+    /**
284
+     * get key size depending on the cipher
285
+     *
286
+     * @param string $cipher
287
+     * @return int
288
+     * @throws \InvalidArgumentException
289
+     */
290
+    protected function getKeySize($cipher) {
291
+        if (isset($this->supportedCiphersAndKeySize[$cipher])) {
292
+            return $this->supportedCiphersAndKeySize[$cipher];
293
+        }
294
+
295
+        throw new \InvalidArgumentException(
296
+            sprintf(
297
+                    'Unsupported cipher (%s) defined.',
298
+                    $cipher
299
+            )
300
+        );
301
+    }
302
+
303
+    /**
304
+     * get legacy cipher
305
+     *
306
+     * @return string
307
+     */
308
+    public function getLegacyCipher() {
309
+        if (!$this->supportLegacy) {
310
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
311
+        }
312
+
313
+        return self::LEGACY_CIPHER;
314
+    }
315
+
316
+    /**
317
+     * @param string $encryptedContent
318
+     * @param string $iv
319
+     * @return string
320
+     */
321
+    private function concatIV($encryptedContent, $iv) {
322
+        return $encryptedContent . '00iv00' . $iv;
323
+    }
324
+
325
+    /**
326
+     * @param string $encryptedContent
327
+     * @param string $signature
328
+     * @return string
329
+     */
330
+    private function concatSig($encryptedContent, $signature) {
331
+        return $encryptedContent . '00sig00' . $signature;
332
+    }
333
+
334
+    /**
335
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
336
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
337
+     * encrypted content and is not used in any crypto primitive.
338
+     *
339
+     * @param string $data
340
+     * @return string
341
+     */
342
+    private function addPadding($data) {
343
+        return $data . 'xxx';
344
+    }
345
+
346
+    /**
347
+     * generate password hash used to encrypt the users private key
348
+     *
349
+     * @param string $password
350
+     * @param string $cipher
351
+     * @param string $uid only used for user keys
352
+     * @return string
353
+     */
354
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
355
+        $instanceId = $this->config->getSystemValue('instanceid');
356
+        $instanceSecret = $this->config->getSystemValue('secret');
357
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
358
+        $keySize = $this->getKeySize($cipher);
359
+
360
+        $hash = hash_pbkdf2(
361
+            'sha256',
362
+            $password,
363
+            $salt,
364
+            100000,
365
+            $keySize,
366
+            true
367
+        );
368
+
369
+        return $hash;
370
+    }
371
+
372
+    /**
373
+     * encrypt private key
374
+     *
375
+     * @param string $privateKey
376
+     * @param string $password
377
+     * @param string $uid for regular users, empty for system keys
378
+     * @return false|string
379
+     */
380
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
381
+        $cipher = $this->getCipher();
382
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
383
+        $encryptedKey = $this->symmetricEncryptFileContent(
384
+            $privateKey,
385
+            $hash,
386
+            0,
387
+            0
388
+        );
389
+
390
+        return $encryptedKey;
391
+    }
392
+
393
+    /**
394
+     * @param string $privateKey
395
+     * @param string $password
396
+     * @param string $uid for regular users, empty for system keys
397
+     * @return false|string
398
+     */
399
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
400
+        $header = $this->parseHeader($privateKey);
401
+
402
+        if (isset($header['cipher'])) {
403
+            $cipher = $header['cipher'];
404
+        } else {
405
+            $cipher = $this->getLegacyCipher();
406
+        }
407
+
408
+        if (isset($header['keyFormat'])) {
409
+            $keyFormat = $header['keyFormat'];
410
+        } else {
411
+            $keyFormat = self::LEGACY_KEY_FORMAT;
412
+        }
413
+
414
+        if ($keyFormat === 'hash') {
415
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
416
+        }
417
+
418
+        // If we found a header we need to remove it from the key we want to decrypt
419
+        if (!empty($header)) {
420
+            $privateKey = substr($privateKey,
421
+                strpos($privateKey,
422
+                    self::HEADER_END) + strlen(self::HEADER_END));
423
+        }
424
+
425
+        $plainKey = $this->symmetricDecryptFileContent(
426
+            $privateKey,
427
+            $password,
428
+            $cipher,
429
+            0
430
+        );
431
+
432
+        if ($this->isValidPrivateKey($plainKey) === false) {
433
+            return false;
434
+        }
435
+
436
+        return $plainKey;
437
+    }
438
+
439
+    /**
440
+     * check if it is a valid private key
441
+     *
442
+     * @param string $plainKey
443
+     * @return bool
444
+     */
445
+    protected function isValidPrivateKey($plainKey) {
446
+        $res = openssl_get_privatekey($plainKey);
447
+        if (is_resource($res)) {
448
+            $sslInfo = openssl_pkey_get_details($res);
449
+            if (isset($sslInfo['key'])) {
450
+                return true;
451
+            }
452
+        }
453
+
454
+        return false;
455
+    }
456
+
457
+    /**
458
+     * @param string $keyFileContents
459
+     * @param string $passPhrase
460
+     * @param string $cipher
461
+     * @param int $version
462
+     * @param int|string $position
463
+     * @return string
464
+     * @throws DecryptionFailedException
465
+     */
466
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
467
+        if ($keyFileContents == '') {
468
+            return '';
469
+        }
470
+
471
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
472
+
473
+        if ($catFile['signature'] !== false) {
474
+            try {
475
+                // First try the new format
476
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
477
+            } catch (GenericEncryptionException $e) {
478
+                // For compatibility with old files check the version without _
479
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
480
+            }
481
+        }
482
+
483
+        return $this->decrypt($catFile['encrypted'],
484
+            $catFile['iv'],
485
+            $passPhrase,
486
+            $cipher);
487
+    }
488
+
489
+    /**
490
+     * check for valid signature
491
+     *
492
+     * @param string $data
493
+     * @param string $passPhrase
494
+     * @param string $expectedSignature
495
+     * @throws GenericEncryptionException
496
+     */
497
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
498
+        $enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
499
+
500
+        $signature = $this->createSignature($data, $passPhrase);
501
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
502
+
503
+        if (!$isCorrectHash && $enforceSignature) {
504
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
505
+        } elseif (!$isCorrectHash && !$enforceSignature) {
506
+            $this->logger->info("Signature check skipped", ['app' => 'encryption']);
507
+        }
508
+    }
509
+
510
+    /**
511
+     * create signature
512
+     *
513
+     * @param string $data
514
+     * @param string $passPhrase
515
+     * @return string
516
+     */
517
+    private function createSignature($data, $passPhrase) {
518
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
519
+        return hash_hmac('sha256', $data, $passPhrase);
520
+    }
521
+
522
+
523
+    /**
524
+     * remove padding
525
+     *
526
+     * @param string $padded
527
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
528
+     * @return string|false
529
+     */
530
+    private function removePadding($padded, $hasSignature = false) {
531
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
532
+            return substr($padded, 0, -2);
533
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
534
+            return substr($padded, 0, -3);
535
+        }
536
+        return false;
537
+    }
538
+
539
+    /**
540
+     * split meta data from encrypted file
541
+     * Note: for now, we assume that the meta data always start with the iv
542
+     *       followed by the signature, if available
543
+     *
544
+     * @param string $catFile
545
+     * @param string $cipher
546
+     * @return array
547
+     */
548
+    private function splitMetaData($catFile, $cipher) {
549
+        if ($this->hasSignature($catFile, $cipher)) {
550
+            $catFile = $this->removePadding($catFile, true);
551
+            $meta = substr($catFile, -93);
552
+            $iv = substr($meta, strlen('00iv00'), 16);
553
+            $sig = substr($meta, 22 + strlen('00sig00'));
554
+            $encrypted = substr($catFile, 0, -93);
555
+        } else {
556
+            $catFile = $this->removePadding($catFile);
557
+            $meta = substr($catFile, -22);
558
+            $iv = substr($meta, -16);
559
+            $sig = false;
560
+            $encrypted = substr($catFile, 0, -22);
561
+        }
562
+
563
+        return [
564
+            'encrypted' => $encrypted,
565
+            'iv' => $iv,
566
+            'signature' => $sig
567
+        ];
568
+    }
569
+
570
+    /**
571
+     * check if encrypted block is signed
572
+     *
573
+     * @param string $catFile
574
+     * @param string $cipher
575
+     * @return bool
576
+     * @throws GenericEncryptionException
577
+     */
578
+    private function hasSignature($catFile, $cipher) {
579
+        $skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
580
+
581
+        $meta = substr($catFile, -93);
582
+        $signaturePosition = strpos($meta, '00sig00');
583
+
584
+        // If we no longer support the legacy format then everything needs a signature
585
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
586
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
587
+        }
588
+
589
+        // enforce signature for the new 'CTR' ciphers
590
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
591
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
592
+        }
593
+
594
+        return ($signaturePosition !== false);
595
+    }
596
+
597
+
598
+    /**
599
+     * @param string $encryptedContent
600
+     * @param string $iv
601
+     * @param string $passPhrase
602
+     * @param string $cipher
603
+     * @return string
604
+     * @throws DecryptionFailedException
605
+     */
606
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
607
+        $plainContent = openssl_decrypt($encryptedContent,
608
+            $cipher,
609
+            $passPhrase,
610
+            false,
611
+            $iv);
612
+
613
+        if ($plainContent) {
614
+            return $plainContent;
615
+        } else {
616
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
617
+        }
618
+    }
619
+
620
+    /**
621
+     * @param string $data
622
+     * @return array
623
+     */
624
+    protected function parseHeader($data) {
625
+        $result = [];
626
+
627
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
628
+            $endAt = strpos($data, self::HEADER_END);
629
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
630
+
631
+            // +1 not to start with an ':' which would result in empty element at the beginning
632
+            $exploded = explode(':',
633
+                substr($header, strlen(self::HEADER_START) + 1));
634
+
635
+            $element = array_shift($exploded);
636
+
637
+            while ($element !== self::HEADER_END) {
638
+                $result[$element] = array_shift($exploded);
639
+                $element = array_shift($exploded);
640
+            }
641
+        }
642
+
643
+        return $result;
644
+    }
645
+
646
+    /**
647
+     * generate initialization vector
648
+     *
649
+     * @return string
650
+     * @throws GenericEncryptionException
651
+     */
652
+    private function generateIv() {
653
+        return random_bytes(16);
654
+    }
655
+
656
+    /**
657
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
658
+     * as file key
659
+     *
660
+     * @return string
661
+     * @throws \Exception
662
+     */
663
+    public function generateFileKey() {
664
+        return random_bytes(32);
665
+    }
666
+
667
+    /**
668
+     * @param $encKeyFile
669
+     * @param $shareKey
670
+     * @param $privateKey
671
+     * @return string
672
+     * @throws MultiKeyDecryptException
673
+     */
674
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
675
+        if (!$encKeyFile) {
676
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
677
+        }
678
+
679
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
680
+            return $plainContent;
681
+        } else {
682
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
683
+        }
684
+    }
685
+
686
+    /**
687
+     * @param string $plainContent
688
+     * @param array $keyFiles
689
+     * @return array
690
+     * @throws MultiKeyEncryptException
691
+     */
692
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
693
+        // openssl_seal returns false without errors if plaincontent is empty
694
+        // so trigger our own error
695
+        if (empty($plainContent)) {
696
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
697
+        }
698
+
699
+        // Set empty vars to be set by openssl by reference
700
+        $sealed = '';
701
+        $shareKeys = [];
702
+        $mappedShareKeys = [];
703
+
704
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
705
+            $i = 0;
706
+
707
+            // Ensure each shareKey is labelled with its corresponding key id
708
+            foreach ($keyFiles as $userId => $publicKey) {
709
+                $mappedShareKeys[$userId] = $shareKeys[$i];
710
+                $i++;
711
+            }
712
+
713
+            return [
714
+                'keys' => $mappedShareKeys,
715
+                'data' => $sealed
716
+            ];
717
+        } else {
718
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
719
+        }
720
+    }
721 721
 }
Please login to merge, or discard this patch.
lib/public/Encryption/IEncryptionModule.php 1 patch
Indentation   +156 added lines, -156 removed lines patch added patch discarded remove patch
@@ -37,160 +37,160 @@
 block discarded – undo
37 37
  */
38 38
 interface IEncryptionModule {
39 39
 
40
-	/**
41
-	 * @return string defining the technical unique id
42
-	 * @since 8.1.0
43
-	 */
44
-	public function getId();
45
-
46
-	/**
47
-	 * In comparison to getKey() this function returns a human readable (maybe translated) name
48
-	 *
49
-	 * @return string
50
-	 * @since 8.1.0
51
-	 */
52
-	public function getDisplayName();
53
-
54
-	/**
55
-	 * start receiving chunks from a file. This is the place where you can
56
-	 * perform some initial step before starting encrypting/decrypting the
57
-	 * chunks
58
-	 *
59
-	 * @param string $path to the file
60
-	 * @param string $user who read/write the file (null for public access)
61
-	 * @param string $mode php stream open mode
62
-	 * @param array $header contains the header data read from the file
63
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
64
-	 *
65
-	 * $return array $header contain data as key-value pairs which should be
66
-	 *                       written to the header, in case of a write operation
67
-	 *                       or if no additional data is needed return a empty array
68
-	 * @since 8.1.0
69
-	 */
70
-	public function begin($path, $user, $mode, array $header, array $accessList);
71
-
72
-	/**
73
-	 * last chunk received. This is the place where you can perform some final
74
-	 * operation and return some remaining data if something is left in your
75
-	 * buffer.
76
-	 *
77
-	 * @param string $path to the file
78
-	 * @param string $position id of the last block (looks like "<Number>end")
79
-	 *
80
-	 * @return string remained data which should be written to the file in case
81
-	 *                of a write operation
82
-	 *
83
-	 * @since 8.1.0
84
-	 * @since 9.0.0 parameter $position added
85
-	 */
86
-	public function end($path, $position);
87
-
88
-	/**
89
-	 * encrypt data
90
-	 *
91
-	 * @param string $data you want to encrypt
92
-	 * @param string $position position of the block we want to encrypt (starts with '0')
93
-	 *
94
-	 * @return mixed encrypted data
95
-	 *
96
-	 * @since 8.1.0
97
-	 * @since 9.0.0 parameter $position added
98
-	 */
99
-	public function encrypt($data, $position);
100
-
101
-	/**
102
-	 * decrypt data
103
-	 *
104
-	 * @param string $data you want to decrypt
105
-	 * @param int|string $position position of the block we want to decrypt
106
-	 *
107
-	 * @return mixed decrypted data
108
-	 *
109
-	 * @since 8.1.0
110
-	 * @since 9.0.0 parameter $position added
111
-	 */
112
-	public function decrypt($data, $position);
113
-
114
-	/**
115
-	 * update encrypted file, e.g. give additional users access to the file
116
-	 *
117
-	 * @param string $path path to the file which should be updated
118
-	 * @param string $uid of the user who performs the operation
119
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
120
-	 * @return boolean
121
-	 * @since 8.1.0
122
-	 */
123
-	public function update($path, $uid, array $accessList);
124
-
125
-	/**
126
-	 * should the file be encrypted or not
127
-	 *
128
-	 * @param string $path
129
-	 * @return boolean
130
-	 * @since 8.1.0
131
-	 */
132
-	public function shouldEncrypt($path);
133
-
134
-	/**
135
-	 * get size of the unencrypted payload per block.
136
-	 * ownCloud read/write files with a block size of 8192 byte
137
-	 *
138
-	 * @param bool $signed
139
-	 * @return int
140
-	 * @since 8.1.0 optional parameter $signed was added in 9.0.0
141
-	 */
142
-	public function getUnencryptedBlockSize($signed = false);
143
-
144
-	/**
145
-	 * check if the encryption module is able to read the file,
146
-	 * e.g. if all encryption keys exists
147
-	 *
148
-	 * @param string $path
149
-	 * @param string $uid user for whom we want to check if he can read the file
150
-	 * @return boolean
151
-	 * @since 8.1.0
152
-	 */
153
-	public function isReadable($path, $uid);
154
-
155
-	/**
156
-	 * Initial encryption of all files
157
-	 *
158
-	 * @param InputInterface $input
159
-	 * @param OutputInterface $output write some status information to the terminal during encryption
160
-	 * @since 8.2.0
161
-	 */
162
-	public function encryptAll(InputInterface $input, OutputInterface $output);
163
-
164
-	/**
165
-	 * prepare encryption module to decrypt all files
166
-	 *
167
-	 * @param InputInterface $input
168
-	 * @param OutputInterface $output write some status information to the terminal during encryption
169
-	 * @param $user (optional) for which the files should be decrypted, default = all users
170
-	 * @return bool return false on failure or if it isn't supported by the module
171
-	 * @since 8.2.0
172
-	 */
173
-	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '');
174
-
175
-	/**
176
-	 * Check if the module is ready to be used by that specific user.
177
-	 * In case a module is not ready - because e.g. key pairs have not been generated
178
-	 * upon login this method can return false before any operation starts and might
179
-	 * cause issues during operations.
180
-	 *
181
-	 * @param string $user
182
-	 * @return boolean
183
-	 * @since 9.1.0
184
-	 */
185
-	public function isReadyForUser($user);
186
-
187
-	/**
188
-	 * Does the encryption module needs a detailed list of users with access to the file?
189
-	 * For example if the encryption module uses per-user encryption keys and needs to know
190
-	 * the users with access to the file to encrypt/decrypt it.
191
-	 *
192
-	 * @since 13.0.0
193
-	 * @return bool
194
-	 */
195
-	public function needDetailedAccessList();
40
+    /**
41
+     * @return string defining the technical unique id
42
+     * @since 8.1.0
43
+     */
44
+    public function getId();
45
+
46
+    /**
47
+     * In comparison to getKey() this function returns a human readable (maybe translated) name
48
+     *
49
+     * @return string
50
+     * @since 8.1.0
51
+     */
52
+    public function getDisplayName();
53
+
54
+    /**
55
+     * start receiving chunks from a file. This is the place where you can
56
+     * perform some initial step before starting encrypting/decrypting the
57
+     * chunks
58
+     *
59
+     * @param string $path to the file
60
+     * @param string $user who read/write the file (null for public access)
61
+     * @param string $mode php stream open mode
62
+     * @param array $header contains the header data read from the file
63
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
64
+     *
65
+     * $return array $header contain data as key-value pairs which should be
66
+     *                       written to the header, in case of a write operation
67
+     *                       or if no additional data is needed return a empty array
68
+     * @since 8.1.0
69
+     */
70
+    public function begin($path, $user, $mode, array $header, array $accessList);
71
+
72
+    /**
73
+     * last chunk received. This is the place where you can perform some final
74
+     * operation and return some remaining data if something is left in your
75
+     * buffer.
76
+     *
77
+     * @param string $path to the file
78
+     * @param string $position id of the last block (looks like "<Number>end")
79
+     *
80
+     * @return string remained data which should be written to the file in case
81
+     *                of a write operation
82
+     *
83
+     * @since 8.1.0
84
+     * @since 9.0.0 parameter $position added
85
+     */
86
+    public function end($path, $position);
87
+
88
+    /**
89
+     * encrypt data
90
+     *
91
+     * @param string $data you want to encrypt
92
+     * @param string $position position of the block we want to encrypt (starts with '0')
93
+     *
94
+     * @return mixed encrypted data
95
+     *
96
+     * @since 8.1.0
97
+     * @since 9.0.0 parameter $position added
98
+     */
99
+    public function encrypt($data, $position);
100
+
101
+    /**
102
+     * decrypt data
103
+     *
104
+     * @param string $data you want to decrypt
105
+     * @param int|string $position position of the block we want to decrypt
106
+     *
107
+     * @return mixed decrypted data
108
+     *
109
+     * @since 8.1.0
110
+     * @since 9.0.0 parameter $position added
111
+     */
112
+    public function decrypt($data, $position);
113
+
114
+    /**
115
+     * update encrypted file, e.g. give additional users access to the file
116
+     *
117
+     * @param string $path path to the file which should be updated
118
+     * @param string $uid of the user who performs the operation
119
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
120
+     * @return boolean
121
+     * @since 8.1.0
122
+     */
123
+    public function update($path, $uid, array $accessList);
124
+
125
+    /**
126
+     * should the file be encrypted or not
127
+     *
128
+     * @param string $path
129
+     * @return boolean
130
+     * @since 8.1.0
131
+     */
132
+    public function shouldEncrypt($path);
133
+
134
+    /**
135
+     * get size of the unencrypted payload per block.
136
+     * ownCloud read/write files with a block size of 8192 byte
137
+     *
138
+     * @param bool $signed
139
+     * @return int
140
+     * @since 8.1.0 optional parameter $signed was added in 9.0.0
141
+     */
142
+    public function getUnencryptedBlockSize($signed = false);
143
+
144
+    /**
145
+     * check if the encryption module is able to read the file,
146
+     * e.g. if all encryption keys exists
147
+     *
148
+     * @param string $path
149
+     * @param string $uid user for whom we want to check if he can read the file
150
+     * @return boolean
151
+     * @since 8.1.0
152
+     */
153
+    public function isReadable($path, $uid);
154
+
155
+    /**
156
+     * Initial encryption of all files
157
+     *
158
+     * @param InputInterface $input
159
+     * @param OutputInterface $output write some status information to the terminal during encryption
160
+     * @since 8.2.0
161
+     */
162
+    public function encryptAll(InputInterface $input, OutputInterface $output);
163
+
164
+    /**
165
+     * prepare encryption module to decrypt all files
166
+     *
167
+     * @param InputInterface $input
168
+     * @param OutputInterface $output write some status information to the terminal during encryption
169
+     * @param $user (optional) for which the files should be decrypted, default = all users
170
+     * @return bool return false on failure or if it isn't supported by the module
171
+     * @since 8.2.0
172
+     */
173
+    public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '');
174
+
175
+    /**
176
+     * Check if the module is ready to be used by that specific user.
177
+     * In case a module is not ready - because e.g. key pairs have not been generated
178
+     * upon login this method can return false before any operation starts and might
179
+     * cause issues during operations.
180
+     *
181
+     * @param string $user
182
+     * @return boolean
183
+     * @since 9.1.0
184
+     */
185
+    public function isReadyForUser($user);
186
+
187
+    /**
188
+     * Does the encryption module needs a detailed list of users with access to the file?
189
+     * For example if the encryption module uses per-user encryption keys and needs to know
190
+     * the users with access to the file to encrypt/decrypt it.
191
+     *
192
+     * @since 13.0.0
193
+     * @return bool
194
+     */
195
+    public function needDetailedAccessList();
196 196
 }
Please login to merge, or discard this patch.