Passed
Push — master ( c9e182...08209e )
by Roeland
123:15 queued 106:48
created
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +655 added lines, -655 removed lines patch added patch discarded remove patch
@@ -57,659 +57,659 @@
 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
-		return $cipher;
274
-	}
275
-
276
-	/**
277
-	 * get key size depending on the cipher
278
-	 *
279
-	 * @param string $cipher
280
-	 * @return int
281
-	 * @throws \InvalidArgumentException
282
-	 */
283
-	protected function getKeySize($cipher) {
284
-		if (isset($this->supportedCiphersAndKeySize[$cipher])) {
285
-			return $this->supportedCiphersAndKeySize[$cipher];
286
-		}
287
-
288
-		throw new \InvalidArgumentException(
289
-			sprintf(
290
-					'Unsupported cipher (%s) defined.',
291
-					$cipher
292
-			)
293
-		);
294
-	}
295
-
296
-	/**
297
-	 * get legacy cipher
298
-	 *
299
-	 * @return string
300
-	 */
301
-	public function getLegacyCipher() {
302
-		if (!$this->supportLegacy) {
303
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
304
-		}
305
-
306
-		return self::LEGACY_CIPHER;
307
-	}
308
-
309
-	/**
310
-	 * @param string $encryptedContent
311
-	 * @param string $iv
312
-	 * @return string
313
-	 */
314
-	private function concatIV($encryptedContent, $iv) {
315
-		return $encryptedContent . '00iv00' . $iv;
316
-	}
317
-
318
-	/**
319
-	 * @param string $encryptedContent
320
-	 * @param string $signature
321
-	 * @return string
322
-	 */
323
-	private function concatSig($encryptedContent, $signature) {
324
-		return $encryptedContent . '00sig00' . $signature;
325
-	}
326
-
327
-	/**
328
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
329
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
330
-	 * encrypted content and is not used in any crypto primitive.
331
-	 *
332
-	 * @param string $data
333
-	 * @return string
334
-	 */
335
-	private function addPadding($data) {
336
-		return $data . 'xxx';
337
-	}
338
-
339
-	/**
340
-	 * generate password hash used to encrypt the users private key
341
-	 *
342
-	 * @param string $password
343
-	 * @param string $cipher
344
-	 * @param string $uid only used for user keys
345
-	 * @return string
346
-	 */
347
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
348
-		$instanceId = $this->config->getSystemValue('instanceid');
349
-		$instanceSecret = $this->config->getSystemValue('secret');
350
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
351
-		$keySize = $this->getKeySize($cipher);
352
-
353
-		$hash = hash_pbkdf2(
354
-			'sha256',
355
-			$password,
356
-			$salt,
357
-			100000,
358
-			$keySize,
359
-			true
360
-		);
361
-
362
-		return $hash;
363
-	}
364
-
365
-	/**
366
-	 * encrypt private key
367
-	 *
368
-	 * @param string $privateKey
369
-	 * @param string $password
370
-	 * @param string $uid for regular users, empty for system keys
371
-	 * @return false|string
372
-	 */
373
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
374
-		$cipher = $this->getCipher();
375
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
376
-		$encryptedKey = $this->symmetricEncryptFileContent(
377
-			$privateKey,
378
-			$hash,
379
-			0,
380
-			0
381
-		);
382
-
383
-		return $encryptedKey;
384
-	}
385
-
386
-	/**
387
-	 * @param string $privateKey
388
-	 * @param string $password
389
-	 * @param string $uid for regular users, empty for system keys
390
-	 * @return false|string
391
-	 */
392
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
393
-		$header = $this->parseHeader($privateKey);
394
-
395
-		if (isset($header['cipher'])) {
396
-			$cipher = $header['cipher'];
397
-		} else {
398
-			$cipher = $this->getLegacyCipher();
399
-		}
400
-
401
-		if (isset($header['keyFormat'])) {
402
-			$keyFormat = $header['keyFormat'];
403
-		} else {
404
-			$keyFormat = self::LEGACY_KEY_FORMAT;
405
-		}
406
-
407
-		if ($keyFormat === 'hash') {
408
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
409
-		}
410
-
411
-		// If we found a header we need to remove it from the key we want to decrypt
412
-		if (!empty($header)) {
413
-			$privateKey = substr($privateKey,
414
-				strpos($privateKey,
415
-					self::HEADER_END) + strlen(self::HEADER_END));
416
-		}
417
-
418
-		$plainKey = $this->symmetricDecryptFileContent(
419
-			$privateKey,
420
-			$password,
421
-			$cipher,
422
-			0
423
-		);
424
-
425
-		if ($this->isValidPrivateKey($plainKey) === false) {
426
-			return false;
427
-		}
428
-
429
-		return $plainKey;
430
-	}
431
-
432
-	/**
433
-	 * check if it is a valid private key
434
-	 *
435
-	 * @param string $plainKey
436
-	 * @return bool
437
-	 */
438
-	protected function isValidPrivateKey($plainKey) {
439
-		$res = openssl_get_privatekey($plainKey);
440
-		// TODO: remove resource check one php7.4 is not longer supported
441
-		if (is_resource($res) || (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey')) {
442
-			$sslInfo = openssl_pkey_get_details($res);
443
-			if (isset($sslInfo['key'])) {
444
-				return true;
445
-			}
446
-		}
447
-
448
-		return false;
449
-	}
450
-
451
-	/**
452
-	 * @param string $keyFileContents
453
-	 * @param string $passPhrase
454
-	 * @param string $cipher
455
-	 * @param int $version
456
-	 * @param int|string $position
457
-	 * @return string
458
-	 * @throws DecryptionFailedException
459
-	 */
460
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
461
-		if ($keyFileContents == '') {
462
-			return '';
463
-		}
464
-
465
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
466
-
467
-		if ($catFile['signature'] !== false) {
468
-			try {
469
-				// First try the new format
470
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
471
-			} catch (GenericEncryptionException $e) {
472
-				// For compatibility with old files check the version without _
473
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
474
-			}
475
-		}
476
-
477
-		return $this->decrypt($catFile['encrypted'],
478
-			$catFile['iv'],
479
-			$passPhrase,
480
-			$cipher);
481
-	}
482
-
483
-	/**
484
-	 * check for valid signature
485
-	 *
486
-	 * @param string $data
487
-	 * @param string $passPhrase
488
-	 * @param string $expectedSignature
489
-	 * @throws GenericEncryptionException
490
-	 */
491
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
492
-		$enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
493
-
494
-		$signature = $this->createSignature($data, $passPhrase);
495
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
496
-
497
-		if (!$isCorrectHash && $enforceSignature) {
498
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
499
-		} elseif (!$isCorrectHash && !$enforceSignature) {
500
-			$this->logger->info("Signature check skipped", ['app' => 'encryption']);
501
-		}
502
-	}
503
-
504
-	/**
505
-	 * create signature
506
-	 *
507
-	 * @param string $data
508
-	 * @param string $passPhrase
509
-	 * @return string
510
-	 */
511
-	private function createSignature($data, $passPhrase) {
512
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
513
-		return hash_hmac('sha256', $data, $passPhrase);
514
-	}
515
-
516
-
517
-	/**
518
-	 * remove padding
519
-	 *
520
-	 * @param string $padded
521
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
522
-	 * @return string|false
523
-	 */
524
-	private function removePadding($padded, $hasSignature = false) {
525
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
526
-			return substr($padded, 0, -2);
527
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
528
-			return substr($padded, 0, -3);
529
-		}
530
-		return false;
531
-	}
532
-
533
-	/**
534
-	 * split meta data from encrypted file
535
-	 * Note: for now, we assume that the meta data always start with the iv
536
-	 *       followed by the signature, if available
537
-	 *
538
-	 * @param string $catFile
539
-	 * @param string $cipher
540
-	 * @return array
541
-	 */
542
-	private function splitMetaData($catFile, $cipher) {
543
-		if ($this->hasSignature($catFile, $cipher)) {
544
-			$catFile = $this->removePadding($catFile, true);
545
-			$meta = substr($catFile, -93);
546
-			$iv = substr($meta, strlen('00iv00'), 16);
547
-			$sig = substr($meta, 22 + strlen('00sig00'));
548
-			$encrypted = substr($catFile, 0, -93);
549
-		} else {
550
-			$catFile = $this->removePadding($catFile);
551
-			$meta = substr($catFile, -22);
552
-			$iv = substr($meta, -16);
553
-			$sig = false;
554
-			$encrypted = substr($catFile, 0, -22);
555
-		}
556
-
557
-		return [
558
-			'encrypted' => $encrypted,
559
-			'iv' => $iv,
560
-			'signature' => $sig
561
-		];
562
-	}
563
-
564
-	/**
565
-	 * check if encrypted block is signed
566
-	 *
567
-	 * @param string $catFile
568
-	 * @param string $cipher
569
-	 * @return bool
570
-	 * @throws GenericEncryptionException
571
-	 */
572
-	private function hasSignature($catFile, $cipher) {
573
-		$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
574
-
575
-		$meta = substr($catFile, -93);
576
-		$signaturePosition = strpos($meta, '00sig00');
577
-
578
-		// If we no longer support the legacy format then everything needs a signature
579
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
580
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
581
-		}
582
-
583
-		// enforce signature for the new 'CTR' ciphers
584
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
585
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
586
-		}
587
-
588
-		return ($signaturePosition !== false);
589
-	}
590
-
591
-
592
-	/**
593
-	 * @param string $encryptedContent
594
-	 * @param string $iv
595
-	 * @param string $passPhrase
596
-	 * @param string $cipher
597
-	 * @return string
598
-	 * @throws DecryptionFailedException
599
-	 */
600
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
601
-		$plainContent = openssl_decrypt($encryptedContent,
602
-			$cipher,
603
-			$passPhrase,
604
-			false,
605
-			$iv);
606
-
607
-		if ($plainContent) {
608
-			return $plainContent;
609
-		} else {
610
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
611
-		}
612
-	}
613
-
614
-	/**
615
-	 * @param string $data
616
-	 * @return array
617
-	 */
618
-	protected function parseHeader($data) {
619
-		$result = [];
620
-
621
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
622
-			$endAt = strpos($data, self::HEADER_END);
623
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
624
-
625
-			// +1 not to start with an ':' which would result in empty element at the beginning
626
-			$exploded = explode(':',
627
-				substr($header, strlen(self::HEADER_START) + 1));
628
-
629
-			$element = array_shift($exploded);
630
-
631
-			while ($element !== self::HEADER_END) {
632
-				$result[$element] = array_shift($exploded);
633
-				$element = array_shift($exploded);
634
-			}
635
-		}
636
-
637
-		return $result;
638
-	}
639
-
640
-	/**
641
-	 * generate initialization vector
642
-	 *
643
-	 * @return string
644
-	 * @throws GenericEncryptionException
645
-	 */
646
-	private function generateIv() {
647
-		return random_bytes(16);
648
-	}
649
-
650
-	/**
651
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
652
-	 * as file key
653
-	 *
654
-	 * @return string
655
-	 * @throws \Exception
656
-	 */
657
-	public function generateFileKey() {
658
-		return random_bytes(32);
659
-	}
660
-
661
-	/**
662
-	 * @param $encKeyFile
663
-	 * @param $shareKey
664
-	 * @param $privateKey
665
-	 * @return string
666
-	 * @throws MultiKeyDecryptException
667
-	 */
668
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
669
-		if (!$encKeyFile) {
670
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
671
-		}
672
-
673
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
674
-			return $plainContent;
675
-		} else {
676
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
677
-		}
678
-	}
679
-
680
-	/**
681
-	 * @param string $plainContent
682
-	 * @param array $keyFiles
683
-	 * @return array
684
-	 * @throws MultiKeyEncryptException
685
-	 */
686
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
687
-		// openssl_seal returns false without errors if plaincontent is empty
688
-		// so trigger our own error
689
-		if (empty($plainContent)) {
690
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
691
-		}
692
-
693
-		// Set empty vars to be set by openssl by reference
694
-		$sealed = '';
695
-		$shareKeys = [];
696
-		$mappedShareKeys = [];
697
-
698
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
699
-			$i = 0;
700
-
701
-			// Ensure each shareKey is labelled with its corresponding key id
702
-			foreach ($keyFiles as $userId => $publicKey) {
703
-				$mappedShareKeys[$userId] = $shareKeys[$i];
704
-				$i++;
705
-			}
706
-
707
-			return [
708
-				'keys' => $mappedShareKeys,
709
-				'data' => $sealed
710
-			];
711
-		} else {
712
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
713
-		}
714
-	}
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
+        return $cipher;
274
+    }
275
+
276
+    /**
277
+     * get key size depending on the cipher
278
+     *
279
+     * @param string $cipher
280
+     * @return int
281
+     * @throws \InvalidArgumentException
282
+     */
283
+    protected function getKeySize($cipher) {
284
+        if (isset($this->supportedCiphersAndKeySize[$cipher])) {
285
+            return $this->supportedCiphersAndKeySize[$cipher];
286
+        }
287
+
288
+        throw new \InvalidArgumentException(
289
+            sprintf(
290
+                    'Unsupported cipher (%s) defined.',
291
+                    $cipher
292
+            )
293
+        );
294
+    }
295
+
296
+    /**
297
+     * get legacy cipher
298
+     *
299
+     * @return string
300
+     */
301
+    public function getLegacyCipher() {
302
+        if (!$this->supportLegacy) {
303
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
304
+        }
305
+
306
+        return self::LEGACY_CIPHER;
307
+    }
308
+
309
+    /**
310
+     * @param string $encryptedContent
311
+     * @param string $iv
312
+     * @return string
313
+     */
314
+    private function concatIV($encryptedContent, $iv) {
315
+        return $encryptedContent . '00iv00' . $iv;
316
+    }
317
+
318
+    /**
319
+     * @param string $encryptedContent
320
+     * @param string $signature
321
+     * @return string
322
+     */
323
+    private function concatSig($encryptedContent, $signature) {
324
+        return $encryptedContent . '00sig00' . $signature;
325
+    }
326
+
327
+    /**
328
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
329
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
330
+     * encrypted content and is not used in any crypto primitive.
331
+     *
332
+     * @param string $data
333
+     * @return string
334
+     */
335
+    private function addPadding($data) {
336
+        return $data . 'xxx';
337
+    }
338
+
339
+    /**
340
+     * generate password hash used to encrypt the users private key
341
+     *
342
+     * @param string $password
343
+     * @param string $cipher
344
+     * @param string $uid only used for user keys
345
+     * @return string
346
+     */
347
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
348
+        $instanceId = $this->config->getSystemValue('instanceid');
349
+        $instanceSecret = $this->config->getSystemValue('secret');
350
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
351
+        $keySize = $this->getKeySize($cipher);
352
+
353
+        $hash = hash_pbkdf2(
354
+            'sha256',
355
+            $password,
356
+            $salt,
357
+            100000,
358
+            $keySize,
359
+            true
360
+        );
361
+
362
+        return $hash;
363
+    }
364
+
365
+    /**
366
+     * encrypt private key
367
+     *
368
+     * @param string $privateKey
369
+     * @param string $password
370
+     * @param string $uid for regular users, empty for system keys
371
+     * @return false|string
372
+     */
373
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
374
+        $cipher = $this->getCipher();
375
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
376
+        $encryptedKey = $this->symmetricEncryptFileContent(
377
+            $privateKey,
378
+            $hash,
379
+            0,
380
+            0
381
+        );
382
+
383
+        return $encryptedKey;
384
+    }
385
+
386
+    /**
387
+     * @param string $privateKey
388
+     * @param string $password
389
+     * @param string $uid for regular users, empty for system keys
390
+     * @return false|string
391
+     */
392
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
393
+        $header = $this->parseHeader($privateKey);
394
+
395
+        if (isset($header['cipher'])) {
396
+            $cipher = $header['cipher'];
397
+        } else {
398
+            $cipher = $this->getLegacyCipher();
399
+        }
400
+
401
+        if (isset($header['keyFormat'])) {
402
+            $keyFormat = $header['keyFormat'];
403
+        } else {
404
+            $keyFormat = self::LEGACY_KEY_FORMAT;
405
+        }
406
+
407
+        if ($keyFormat === 'hash') {
408
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
409
+        }
410
+
411
+        // If we found a header we need to remove it from the key we want to decrypt
412
+        if (!empty($header)) {
413
+            $privateKey = substr($privateKey,
414
+                strpos($privateKey,
415
+                    self::HEADER_END) + strlen(self::HEADER_END));
416
+        }
417
+
418
+        $plainKey = $this->symmetricDecryptFileContent(
419
+            $privateKey,
420
+            $password,
421
+            $cipher,
422
+            0
423
+        );
424
+
425
+        if ($this->isValidPrivateKey($plainKey) === false) {
426
+            return false;
427
+        }
428
+
429
+        return $plainKey;
430
+    }
431
+
432
+    /**
433
+     * check if it is a valid private key
434
+     *
435
+     * @param string $plainKey
436
+     * @return bool
437
+     */
438
+    protected function isValidPrivateKey($plainKey) {
439
+        $res = openssl_get_privatekey($plainKey);
440
+        // TODO: remove resource check one php7.4 is not longer supported
441
+        if (is_resource($res) || (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey')) {
442
+            $sslInfo = openssl_pkey_get_details($res);
443
+            if (isset($sslInfo['key'])) {
444
+                return true;
445
+            }
446
+        }
447
+
448
+        return false;
449
+    }
450
+
451
+    /**
452
+     * @param string $keyFileContents
453
+     * @param string $passPhrase
454
+     * @param string $cipher
455
+     * @param int $version
456
+     * @param int|string $position
457
+     * @return string
458
+     * @throws DecryptionFailedException
459
+     */
460
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
461
+        if ($keyFileContents == '') {
462
+            return '';
463
+        }
464
+
465
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
466
+
467
+        if ($catFile['signature'] !== false) {
468
+            try {
469
+                // First try the new format
470
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
471
+            } catch (GenericEncryptionException $e) {
472
+                // For compatibility with old files check the version without _
473
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
474
+            }
475
+        }
476
+
477
+        return $this->decrypt($catFile['encrypted'],
478
+            $catFile['iv'],
479
+            $passPhrase,
480
+            $cipher);
481
+    }
482
+
483
+    /**
484
+     * check for valid signature
485
+     *
486
+     * @param string $data
487
+     * @param string $passPhrase
488
+     * @param string $expectedSignature
489
+     * @throws GenericEncryptionException
490
+     */
491
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
492
+        $enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
493
+
494
+        $signature = $this->createSignature($data, $passPhrase);
495
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
496
+
497
+        if (!$isCorrectHash && $enforceSignature) {
498
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
499
+        } elseif (!$isCorrectHash && !$enforceSignature) {
500
+            $this->logger->info("Signature check skipped", ['app' => 'encryption']);
501
+        }
502
+    }
503
+
504
+    /**
505
+     * create signature
506
+     *
507
+     * @param string $data
508
+     * @param string $passPhrase
509
+     * @return string
510
+     */
511
+    private function createSignature($data, $passPhrase) {
512
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
513
+        return hash_hmac('sha256', $data, $passPhrase);
514
+    }
515
+
516
+
517
+    /**
518
+     * remove padding
519
+     *
520
+     * @param string $padded
521
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
522
+     * @return string|false
523
+     */
524
+    private function removePadding($padded, $hasSignature = false) {
525
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
526
+            return substr($padded, 0, -2);
527
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
528
+            return substr($padded, 0, -3);
529
+        }
530
+        return false;
531
+    }
532
+
533
+    /**
534
+     * split meta data from encrypted file
535
+     * Note: for now, we assume that the meta data always start with the iv
536
+     *       followed by the signature, if available
537
+     *
538
+     * @param string $catFile
539
+     * @param string $cipher
540
+     * @return array
541
+     */
542
+    private function splitMetaData($catFile, $cipher) {
543
+        if ($this->hasSignature($catFile, $cipher)) {
544
+            $catFile = $this->removePadding($catFile, true);
545
+            $meta = substr($catFile, -93);
546
+            $iv = substr($meta, strlen('00iv00'), 16);
547
+            $sig = substr($meta, 22 + strlen('00sig00'));
548
+            $encrypted = substr($catFile, 0, -93);
549
+        } else {
550
+            $catFile = $this->removePadding($catFile);
551
+            $meta = substr($catFile, -22);
552
+            $iv = substr($meta, -16);
553
+            $sig = false;
554
+            $encrypted = substr($catFile, 0, -22);
555
+        }
556
+
557
+        return [
558
+            'encrypted' => $encrypted,
559
+            'iv' => $iv,
560
+            'signature' => $sig
561
+        ];
562
+    }
563
+
564
+    /**
565
+     * check if encrypted block is signed
566
+     *
567
+     * @param string $catFile
568
+     * @param string $cipher
569
+     * @return bool
570
+     * @throws GenericEncryptionException
571
+     */
572
+    private function hasSignature($catFile, $cipher) {
573
+        $skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
574
+
575
+        $meta = substr($catFile, -93);
576
+        $signaturePosition = strpos($meta, '00sig00');
577
+
578
+        // If we no longer support the legacy format then everything needs a signature
579
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
580
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
581
+        }
582
+
583
+        // enforce signature for the new 'CTR' ciphers
584
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
585
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
586
+        }
587
+
588
+        return ($signaturePosition !== false);
589
+    }
590
+
591
+
592
+    /**
593
+     * @param string $encryptedContent
594
+     * @param string $iv
595
+     * @param string $passPhrase
596
+     * @param string $cipher
597
+     * @return string
598
+     * @throws DecryptionFailedException
599
+     */
600
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
601
+        $plainContent = openssl_decrypt($encryptedContent,
602
+            $cipher,
603
+            $passPhrase,
604
+            false,
605
+            $iv);
606
+
607
+        if ($plainContent) {
608
+            return $plainContent;
609
+        } else {
610
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
611
+        }
612
+    }
613
+
614
+    /**
615
+     * @param string $data
616
+     * @return array
617
+     */
618
+    protected function parseHeader($data) {
619
+        $result = [];
620
+
621
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
622
+            $endAt = strpos($data, self::HEADER_END);
623
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
624
+
625
+            // +1 not to start with an ':' which would result in empty element at the beginning
626
+            $exploded = explode(':',
627
+                substr($header, strlen(self::HEADER_START) + 1));
628
+
629
+            $element = array_shift($exploded);
630
+
631
+            while ($element !== self::HEADER_END) {
632
+                $result[$element] = array_shift($exploded);
633
+                $element = array_shift($exploded);
634
+            }
635
+        }
636
+
637
+        return $result;
638
+    }
639
+
640
+    /**
641
+     * generate initialization vector
642
+     *
643
+     * @return string
644
+     * @throws GenericEncryptionException
645
+     */
646
+    private function generateIv() {
647
+        return random_bytes(16);
648
+    }
649
+
650
+    /**
651
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
652
+     * as file key
653
+     *
654
+     * @return string
655
+     * @throws \Exception
656
+     */
657
+    public function generateFileKey() {
658
+        return random_bytes(32);
659
+    }
660
+
661
+    /**
662
+     * @param $encKeyFile
663
+     * @param $shareKey
664
+     * @param $privateKey
665
+     * @return string
666
+     * @throws MultiKeyDecryptException
667
+     */
668
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
669
+        if (!$encKeyFile) {
670
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
671
+        }
672
+
673
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
674
+            return $plainContent;
675
+        } else {
676
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
677
+        }
678
+    }
679
+
680
+    /**
681
+     * @param string $plainContent
682
+     * @param array $keyFiles
683
+     * @return array
684
+     * @throws MultiKeyEncryptException
685
+     */
686
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
687
+        // openssl_seal returns false without errors if plaincontent is empty
688
+        // so trigger our own error
689
+        if (empty($plainContent)) {
690
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
691
+        }
692
+
693
+        // Set empty vars to be set by openssl by reference
694
+        $sealed = '';
695
+        $shareKeys = [];
696
+        $mappedShareKeys = [];
697
+
698
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
699
+            $i = 0;
700
+
701
+            // Ensure each shareKey is labelled with its corresponding key id
702
+            foreach ($keyFiles as $userId => $publicKey) {
703
+                $mappedShareKeys[$userId] = $shareKeys[$i];
704
+                $i++;
705
+            }
706
+
707
+            return [
708
+                'keys' => $mappedShareKeys,
709
+                'data' => $sealed
710
+            ];
711
+        } else {
712
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
713
+        }
714
+    }
715 715
 }
Please login to merge, or discard this patch.