Passed
Push — master ( 54b570...b95206 )
by Christoph
16:23 queued 13s
created
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +841 added lines, -841 removed lines patch added patch discarded remove patch
@@ -59,845 +59,845 @@
 block discarded – undo
59 59
  * @package OCA\Encryption\Crypto
60 60
  */
61 61
 class Crypt {
62
-	public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
63
-		'AES-256-CTR' => 32,
64
-		'AES-128-CTR' => 16,
65
-		'AES-256-CFB' => 32,
66
-		'AES-128-CFB' => 16,
67
-	];
68
-	// one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
69
-	public const DEFAULT_CIPHER = 'AES-256-CTR';
70
-	// default cipher from old Nextcloud versions
71
-	public const LEGACY_CIPHER = 'AES-128-CFB';
72
-
73
-	public const SUPPORTED_KEY_FORMATS = ['hash', 'password'];
74
-	// one out of SUPPORTED_KEY_FORMATS
75
-	public const DEFAULT_KEY_FORMAT = 'hash';
76
-	// default key format, old Nextcloud version encrypted the private key directly
77
-	// with the user password
78
-	public const LEGACY_KEY_FORMAT = 'password';
79
-
80
-	public const HEADER_START = 'HBEGIN';
81
-	public const HEADER_END = 'HEND';
82
-
83
-	// default encoding format, old Nextcloud versions used base64
84
-	public const BINARY_ENCODING_FORMAT = 'binary';
85
-
86
-	/** @var ILogger */
87
-	private $logger;
88
-
89
-	/** @var string */
90
-	private $user;
91
-
92
-	/** @var IConfig */
93
-	private $config;
94
-
95
-	/** @var IL10N */
96
-	private $l;
97
-
98
-	/** @var string|null */
99
-	private $currentCipher;
100
-
101
-	/** @var bool */
102
-	private $supportLegacy;
103
-
104
-	/**
105
-	 * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
106
-	 */
107
-	private bool $useLegacyBase64Encoding;
108
-
109
-	/**
110
-	 * @param ILogger $logger
111
-	 * @param IUserSession $userSession
112
-	 * @param IConfig $config
113
-	 * @param IL10N $l
114
-	 */
115
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
116
-		$this->logger = $logger;
117
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
118
-		$this->config = $config;
119
-		$this->l = $l;
120
-		$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
121
-		$this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
122
-	}
123
-
124
-	/**
125
-	 * create new private/public key-pair for user
126
-	 *
127
-	 * @return array|bool
128
-	 */
129
-	public function createKeyPair() {
130
-		$log = $this->logger;
131
-		$res = $this->getOpenSSLPKey();
132
-
133
-		if (!$res) {
134
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
135
-				['app' => 'encryption']);
136
-
137
-			if (openssl_error_string()) {
138
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
139
-					['app' => 'encryption']);
140
-			}
141
-		} elseif (openssl_pkey_export($res,
142
-			$privateKey,
143
-			null,
144
-			$this->getOpenSSLConfig())) {
145
-			$keyDetails = openssl_pkey_get_details($res);
146
-			$publicKey = $keyDetails['key'];
147
-
148
-			return [
149
-				'publicKey' => $publicKey,
150
-				'privateKey' => $privateKey
151
-			];
152
-		}
153
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
154
-			['app' => 'encryption']);
155
-		if (openssl_error_string()) {
156
-			$log->error('Encryption Library:' . openssl_error_string(),
157
-				['app' => 'encryption']);
158
-		}
159
-
160
-		return false;
161
-	}
162
-
163
-	/**
164
-	 * Generates a new private key
165
-	 *
166
-	 * @return \OpenSSLAsymmetricKey|false
167
-	 */
168
-	public function getOpenSSLPKey() {
169
-		$config = $this->getOpenSSLConfig();
170
-		return openssl_pkey_new($config);
171
-	}
172
-
173
-	/**
174
-	 * get openSSL Config
175
-	 *
176
-	 * @return array
177
-	 */
178
-	private function getOpenSSLConfig() {
179
-		$config = ['private_key_bits' => 4096];
180
-		$config = array_merge(
181
-			$config,
182
-			$this->config->getSystemValue('openssl', [])
183
-		);
184
-		return $config;
185
-	}
186
-
187
-	/**
188
-	 * @throws EncryptionFailedException
189
-	 */
190
-	public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false {
191
-		if (!$plainContent) {
192
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
193
-				['app' => 'encryption']);
194
-			return false;
195
-		}
196
-
197
-		$iv = $this->generateIv();
198
-
199
-		$encryptedContent = $this->encrypt($plainContent,
200
-			$iv,
201
-			$passPhrase,
202
-			$this->getCipher());
203
-
204
-		// Create a signature based on the key as well as the current version
205
-		$sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
206
-
207
-		// combine content to encrypt the IV identifier and actual IV
208
-		$catFile = $this->concatIV($encryptedContent, $iv);
209
-		$catFile = $this->concatSig($catFile, $sig);
210
-		return $this->addPadding($catFile);
211
-	}
212
-
213
-	/**
214
-	 * generate header for encrypted file
215
-	 *
216
-	 * @param string $keyFormat see SUPPORTED_KEY_FORMATS
217
-	 * @return string
218
-	 * @throws \InvalidArgumentException
219
-	 */
220
-	public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
221
-		if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
222
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
223
-		}
224
-
225
-		$header = self::HEADER_START
226
-			. ':cipher:' . $this->getCipher()
227
-			. ':keyFormat:' . $keyFormat;
228
-
229
-		if ($this->useLegacyBase64Encoding !== true) {
230
-			$header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
231
-		}
232
-
233
-		$header .= ':' . self::HEADER_END;
234
-
235
-		return $header;
236
-	}
237
-
238
-	/**
239
-	 * @param string $plainContent
240
-	 * @param string $iv
241
-	 * @param string $passPhrase
242
-	 * @param string $cipher
243
-	 * @return string
244
-	 * @throws EncryptionFailedException
245
-	 */
246
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
247
-		$options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
248
-		$encryptedContent = openssl_encrypt($plainContent,
249
-			$cipher,
250
-			$passPhrase,
251
-			$options,
252
-			$iv);
253
-
254
-		if (!$encryptedContent) {
255
-			$error = 'Encryption (symmetric) of content failed';
256
-			$this->logger->error($error . openssl_error_string(),
257
-				['app' => 'encryption']);
258
-			throw new EncryptionFailedException($error);
259
-		}
260
-
261
-		return $encryptedContent;
262
-	}
263
-
264
-	/**
265
-	 * return cipher either from config.php or the default cipher defined in
266
-	 * this class
267
-	 *
268
-	 * @return string
269
-	 */
270
-	private function getCachedCipher() {
271
-		if (isset($this->currentCipher)) {
272
-			return $this->currentCipher;
273
-		}
274
-
275
-		// Get cipher either from config.php or the default cipher defined in this class
276
-		$cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
277
-		if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
278
-			$this->logger->warning(
279
-				sprintf(
280
-					'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
281
-					$cipher,
282
-					self::DEFAULT_CIPHER
283
-				),
284
-				['app' => 'encryption']
285
-			);
286
-			$cipher = self::DEFAULT_CIPHER;
287
-		}
288
-
289
-		// Remember current cipher to avoid frequent lookups
290
-		$this->currentCipher = $cipher;
291
-		return $this->currentCipher;
292
-	}
293
-
294
-	/**
295
-	 * return current encryption cipher
296
-	 *
297
-	 * @return string
298
-	 */
299
-	public function getCipher() {
300
-		return $this->getCachedCipher();
301
-	}
302
-
303
-	/**
304
-	 * get key size depending on the cipher
305
-	 *
306
-	 * @param string $cipher
307
-	 * @return int
308
-	 * @throws \InvalidArgumentException
309
-	 */
310
-	protected function getKeySize($cipher) {
311
-		if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
312
-			return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
313
-		}
314
-
315
-		throw new \InvalidArgumentException(
316
-			sprintf(
317
-				'Unsupported cipher (%s) defined.',
318
-				$cipher
319
-			)
320
-		);
321
-	}
322
-
323
-	/**
324
-	 * get legacy cipher
325
-	 *
326
-	 * @return string
327
-	 */
328
-	public function getLegacyCipher() {
329
-		if (!$this->supportLegacy) {
330
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
331
-		}
332
-
333
-		return self::LEGACY_CIPHER;
334
-	}
335
-
336
-	/**
337
-	 * @param string $encryptedContent
338
-	 * @param string $iv
339
-	 * @return string
340
-	 */
341
-	private function concatIV($encryptedContent, $iv) {
342
-		return $encryptedContent . '00iv00' . $iv;
343
-	}
344
-
345
-	/**
346
-	 * @param string $encryptedContent
347
-	 * @param string $signature
348
-	 * @return string
349
-	 */
350
-	private function concatSig($encryptedContent, $signature) {
351
-		return $encryptedContent . '00sig00' . $signature;
352
-	}
353
-
354
-	/**
355
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
356
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
357
-	 * encrypted content and is not used in any crypto primitive.
358
-	 *
359
-	 * @param string $data
360
-	 * @return string
361
-	 */
362
-	private function addPadding($data) {
363
-		return $data . 'xxx';
364
-	}
365
-
366
-	/**
367
-	 * generate password hash used to encrypt the users private key
368
-	 *
369
-	 * @param string $password
370
-	 * @param string $cipher
371
-	 * @param string $uid only used for user keys
372
-	 * @return string
373
-	 */
374
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
375
-		$instanceId = $this->config->getSystemValue('instanceid');
376
-		$instanceSecret = $this->config->getSystemValue('secret');
377
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
378
-		$keySize = $this->getKeySize($cipher);
379
-
380
-		$hash = hash_pbkdf2(
381
-			'sha256',
382
-			$password,
383
-			$salt,
384
-			100000,
385
-			$keySize,
386
-			true
387
-		);
388
-
389
-		return $hash;
390
-	}
391
-
392
-	/**
393
-	 * encrypt private key
394
-	 *
395
-	 * @param string $privateKey
396
-	 * @param string $password
397
-	 * @param string $uid for regular users, empty for system keys
398
-	 * @return false|string
399
-	 */
400
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
401
-		$cipher = $this->getCipher();
402
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
403
-		$encryptedKey = $this->symmetricEncryptFileContent(
404
-			$privateKey,
405
-			$hash,
406
-			0,
407
-			'0'
408
-		);
409
-
410
-		return $encryptedKey;
411
-	}
412
-
413
-	/**
414
-	 * @param string $privateKey
415
-	 * @param string $password
416
-	 * @param string $uid for regular users, empty for system keys
417
-	 * @return false|string
418
-	 */
419
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
420
-		$header = $this->parseHeader($privateKey);
421
-
422
-		if (isset($header['cipher'])) {
423
-			$cipher = $header['cipher'];
424
-		} else {
425
-			$cipher = $this->getLegacyCipher();
426
-		}
427
-
428
-		if (isset($header['keyFormat'])) {
429
-			$keyFormat = $header['keyFormat'];
430
-		} else {
431
-			$keyFormat = self::LEGACY_KEY_FORMAT;
432
-		}
433
-
434
-		if ($keyFormat === self::DEFAULT_KEY_FORMAT) {
435
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
436
-		}
437
-
438
-		$binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
439
-
440
-		// If we found a header we need to remove it from the key we want to decrypt
441
-		if (!empty($header)) {
442
-			$privateKey = substr($privateKey,
443
-				strpos($privateKey,
444
-					self::HEADER_END) + strlen(self::HEADER_END));
445
-		}
446
-
447
-		$plainKey = $this->symmetricDecryptFileContent(
448
-			$privateKey,
449
-			$password,
450
-			$cipher,
451
-			0,
452
-			0,
453
-			$binaryEncoding
454
-		);
455
-
456
-		if ($this->isValidPrivateKey($plainKey) === false) {
457
-			return false;
458
-		}
459
-
460
-		return $plainKey;
461
-	}
462
-
463
-	/**
464
-	 * check if it is a valid private key
465
-	 *
466
-	 * @param string $plainKey
467
-	 * @return bool
468
-	 */
469
-	protected function isValidPrivateKey($plainKey) {
470
-		$res = openssl_get_privatekey($plainKey);
471
-		if (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey') {
472
-			$sslInfo = openssl_pkey_get_details($res);
473
-			if (isset($sslInfo['key'])) {
474
-				return true;
475
-			}
476
-		}
477
-
478
-		return false;
479
-	}
480
-
481
-	/**
482
-	 * @param string $keyFileContents
483
-	 * @param string $passPhrase
484
-	 * @param string $cipher
485
-	 * @param int $version
486
-	 * @param int|string $position
487
-	 * @param boolean $binaryEncoding
488
-	 * @return string
489
-	 * @throws DecryptionFailedException
490
-	 */
491
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
492
-		if ($keyFileContents == '') {
493
-			return '';
494
-		}
495
-
496
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
497
-
498
-		if ($catFile['signature'] !== false) {
499
-			try {
500
-				// First try the new format
501
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
502
-			} catch (GenericEncryptionException $e) {
503
-				// For compatibility with old files check the version without _
504
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
505
-			}
506
-		}
507
-
508
-		return $this->decrypt($catFile['encrypted'],
509
-			$catFile['iv'],
510
-			$passPhrase,
511
-			$cipher,
512
-			$binaryEncoding);
513
-	}
514
-
515
-	/**
516
-	 * check for valid signature
517
-	 *
518
-	 * @throws GenericEncryptionException
519
-	 */
520
-	private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void {
521
-		$enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
522
-
523
-		$signature = $this->createSignature($data, $passPhrase);
524
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
525
-
526
-		if (!$isCorrectHash) {
527
-			if ($enforceSignature) {
528
-				throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
529
-			} else {
530
-				$this->logger->info("Signature check skipped", ['app' => 'encryption']);
531
-			}
532
-		}
533
-	}
534
-
535
-	/**
536
-	 * create signature
537
-	 */
538
-	private function createSignature(string $data, string $passPhrase): string {
539
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
540
-		return hash_hmac('sha256', $data, $passPhrase);
541
-	}
542
-
543
-
544
-	/**
545
-	 * remove padding
546
-	 *
547
-	 * @param string $padded
548
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
549
-	 * @return string|false
550
-	 */
551
-	private function removePadding($padded, $hasSignature = false) {
552
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
553
-			return substr($padded, 0, -2);
554
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
555
-			return substr($padded, 0, -3);
556
-		}
557
-		return false;
558
-	}
559
-
560
-	/**
561
-	 * split meta data from encrypted file
562
-	 * Note: for now, we assume that the meta data always start with the iv
563
-	 *       followed by the signature, if available
564
-	 *
565
-	 * @param string $catFile
566
-	 * @param string $cipher
567
-	 * @return array
568
-	 */
569
-	private function splitMetaData($catFile, $cipher) {
570
-		if ($this->hasSignature($catFile, $cipher)) {
571
-			$catFile = $this->removePadding($catFile, true);
572
-			$meta = substr($catFile, -93);
573
-			$iv = substr($meta, strlen('00iv00'), 16);
574
-			$sig = substr($meta, 22 + strlen('00sig00'));
575
-			$encrypted = substr($catFile, 0, -93);
576
-		} else {
577
-			$catFile = $this->removePadding($catFile);
578
-			$meta = substr($catFile, -22);
579
-			$iv = substr($meta, -16);
580
-			$sig = false;
581
-			$encrypted = substr($catFile, 0, -22);
582
-		}
583
-
584
-		return [
585
-			'encrypted' => $encrypted,
586
-			'iv' => $iv,
587
-			'signature' => $sig
588
-		];
589
-	}
590
-
591
-	/**
592
-	 * check if encrypted block is signed
593
-	 *
594
-	 * @param string $catFile
595
-	 * @param string $cipher
596
-	 * @return bool
597
-	 * @throws GenericEncryptionException
598
-	 */
599
-	private function hasSignature($catFile, $cipher) {
600
-		$skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
601
-
602
-		$meta = substr($catFile, -93);
603
-		$signaturePosition = strpos($meta, '00sig00');
604
-
605
-		// If we no longer support the legacy format then everything needs a signature
606
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
607
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
608
-		}
609
-
610
-		// Enforce signature for the new 'CTR' ciphers
611
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
612
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
613
-		}
614
-
615
-		return ($signaturePosition !== false);
616
-	}
617
-
618
-
619
-	/**
620
-	 * @param string $encryptedContent
621
-	 * @param string $iv
622
-	 * @param string $passPhrase
623
-	 * @param string $cipher
624
-	 * @param boolean $binaryEncoding
625
-	 * @return string
626
-	 * @throws DecryptionFailedException
627
-	 */
628
-	private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
629
-		$options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
630
-		$plainContent = openssl_decrypt($encryptedContent,
631
-			$cipher,
632
-			$passPhrase,
633
-			$options,
634
-			$iv);
635
-
636
-		if ($plainContent) {
637
-			return $plainContent;
638
-		} else {
639
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
640
-		}
641
-	}
642
-
643
-	/**
644
-	 * @param string $data
645
-	 * @return array
646
-	 */
647
-	protected function parseHeader($data) {
648
-		$result = [];
649
-
650
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
651
-			$endAt = strpos($data, self::HEADER_END);
652
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
653
-
654
-			// +1 not to start with an ':' which would result in empty element at the beginning
655
-			$exploded = explode(':',
656
-				substr($header, strlen(self::HEADER_START) + 1));
657
-
658
-			$element = array_shift($exploded);
659
-
660
-			while ($element !== self::HEADER_END) {
661
-				$result[$element] = array_shift($exploded);
662
-				$element = array_shift($exploded);
663
-			}
664
-		}
665
-
666
-		return $result;
667
-	}
668
-
669
-	/**
670
-	 * generate initialization vector
671
-	 *
672
-	 * @return string
673
-	 * @throws GenericEncryptionException
674
-	 */
675
-	private function generateIv() {
676
-		return random_bytes(16);
677
-	}
678
-
679
-	/**
680
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
681
-	 * as file key
682
-	 *
683
-	 * @return string
684
-	 * @throws \Exception
685
-	 */
686
-	public function generateFileKey() {
687
-		return random_bytes(32);
688
-	}
689
-
690
-	/**
691
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
692
-	 * @throws MultiKeyDecryptException
693
-	 */
694
-	public function multiKeyDecrypt(string $shareKey, $privateKey): string {
695
-		$plainContent = '';
696
-
697
-		// decrypt the intermediate key with RSA
698
-		if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) {
699
-			return $intermediate;
700
-		} else {
701
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
702
-		}
703
-	}
704
-
705
-	/**
706
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
707
-	 * @throws MultiKeyDecryptException
708
-	 */
709
-	public function multiKeyDecryptLegacy(string $encKeyFile, string $shareKey, $privateKey): string {
710
-		if (!$encKeyFile) {
711
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
712
-		}
713
-
714
-		$plainContent = '';
715
-		if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
716
-			return $plainContent;
717
-		} else {
718
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
719
-		}
720
-	}
721
-
722
-	/**
723
-	 * @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string> $keyFiles
724
-	 * @throws MultiKeyEncryptException
725
-	 */
726
-	public function multiKeyEncrypt(string $plainContent, array $keyFiles): array {
727
-		if (empty($plainContent)) {
728
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
729
-		}
730
-
731
-		// Set empty vars to be set by openssl by reference
732
-		$shareKeys = [];
733
-		$mappedShareKeys = [];
734
-
735
-		// make sure that there is at least one public key to use
736
-		if (count($keyFiles) >= 1) {
737
-			// prepare the encrypted keys
738
-			$shareKeys = [];
739
-
740
-			// iterate over the public keys and encrypt the intermediate
741
-			// for each of them with RSA
742
-			foreach ($keyFiles as $tmp_key) {
743
-				if (openssl_public_encrypt($plainContent, $tmp_output, $tmp_key, OPENSSL_PKCS1_OAEP_PADDING)) {
744
-					$shareKeys[] = $tmp_output;
745
-				}
746
-			}
747
-
748
-			// set the result if everything worked fine
749
-			if (count($keyFiles) === count($shareKeys)) {
750
-				$i = 0;
751
-
752
-				// Ensure each shareKey is labelled with its corresponding key id
753
-				foreach ($keyFiles as $userId => $publicKey) {
754
-					$mappedShareKeys[$userId] = $shareKeys[$i];
755
-					$i++;
756
-				}
757
-
758
-				return $mappedShareKeys;
759
-			}
760
-		}
761
-		throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
762
-	}
763
-
764
-	/**
765
-	 * @param string $plainContent
766
-	 * @param array $keyFiles
767
-	 * @return array
768
-	 * @throws MultiKeyEncryptException
769
-	 * @deprecated 27.0.0 use multiKeyEncrypt
770
-	 */
771
-	public function multiKeyEncryptLegacy($plainContent, array $keyFiles) {
772
-		// openssl_seal returns false without errors if plaincontent is empty
773
-		// so trigger our own error
774
-		if (empty($plainContent)) {
775
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
776
-		}
777
-
778
-		// Set empty vars to be set by openssl by reference
779
-		$sealed = '';
780
-		$shareKeys = [];
781
-		$mappedShareKeys = [];
782
-
783
-		if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
784
-			$i = 0;
785
-
786
-			// Ensure each shareKey is labelled with its corresponding key id
787
-			foreach ($keyFiles as $userId => $publicKey) {
788
-				$mappedShareKeys[$userId] = $shareKeys[$i];
789
-				$i++;
790
-			}
791
-
792
-			return [
793
-				'keys' => $mappedShareKeys,
794
-				'data' => $sealed
795
-			];
796
-		} else {
797
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
798
-		}
799
-	}
800
-
801
-	/**
802
-	 * returns the value of $useLegacyBase64Encoding
803
-	 *
804
-	 * @return bool
805
-	 */
806
-	public function useLegacyBase64Encoding(): bool {
807
-		return $this->useLegacyBase64Encoding;
808
-	}
809
-
810
-	/**
811
-	 * Uses phpseclib RC4 implementation
812
-	 */
813
-	private function rc4Decrypt(string $data, string $secret): string {
814
-		$rc4 = new RC4();
815
-		/** @psalm-suppress InternalMethod */
816
-		$rc4->setKey($secret);
817
-
818
-		return $rc4->decrypt($data);
819
-	}
820
-
821
-	/**
822
-	 * Uses phpseclib RC4 implementation
823
-	 */
824
-	private function rc4Encrypt(string $data, string $secret): string {
825
-		$rc4 = new RC4();
826
-		/** @psalm-suppress InternalMethod */
827
-		$rc4->setKey($secret);
828
-
829
-		return $rc4->encrypt($data);
830
-	}
831
-
832
-	/**
833
-	 * Custom implementation of openssl_open()
834
-	 *
835
-	 * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key
836
-	 * @throws DecryptionFailedException
837
-	 */
838
-	private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool {
839
-		$result = false;
840
-
841
-		// check if RC4 is used
842
-		if (strcasecmp($cipher_algo, "rc4") === 0) {
843
-			// decrypt the intermediate key with RSA
844
-			if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) {
845
-				// decrypt the file key with the intermediate key
846
-				// using our own RC4 implementation
847
-				$output = $this->rc4Decrypt($data, $intermediate);
848
-				$result = (strlen($output) === strlen($data));
849
-			}
850
-		} else {
851
-			throw new DecryptionFailedException('Unsupported cipher '.$cipher_algo);
852
-		}
853
-
854
-		return $result;
855
-	}
856
-
857
-	/**
858
-	 * Custom implementation of openssl_seal()
859
-	 *
860
-	 * @deprecated 27.0.0 use multiKeyEncrypt
861
-	 * @throws EncryptionFailedException
862
-	 */
863
-	private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
864
-		$result = false;
865
-
866
-		// check if RC4 is used
867
-		if (strcasecmp($cipher_algo, "rc4") === 0) {
868
-			// make sure that there is at least one public key to use
869
-			if (count($public_key) >= 1) {
870
-				// generate the intermediate key
871
-				$intermediate = openssl_random_pseudo_bytes(16, $strong_result);
872
-
873
-				// check if we got strong random data
874
-				if ($strong_result) {
875
-					// encrypt the file key with the intermediate key
876
-					// using our own RC4 implementation
877
-					$sealed_data = $this->rc4Encrypt($data, $intermediate);
878
-					if (strlen($sealed_data) === strlen($data)) {
879
-						// prepare the encrypted keys
880
-						$encrypted_keys = [];
881
-
882
-						// iterate over the public keys and encrypt the intermediate
883
-						// for each of them with RSA
884
-						foreach ($public_key as $tmp_key) {
885
-							if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) {
886
-								$encrypted_keys[] = $tmp_output;
887
-							}
888
-						}
889
-
890
-						// set the result if everything worked fine
891
-						if (count($public_key) === count($encrypted_keys)) {
892
-							$result = strlen($sealed_data);
893
-						}
894
-					}
895
-				}
896
-			}
897
-		} else {
898
-			throw new EncryptionFailedException('Unsupported cipher '.$cipher_algo);
899
-		}
900
-
901
-		return $result;
902
-	}
62
+    public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
63
+        'AES-256-CTR' => 32,
64
+        'AES-128-CTR' => 16,
65
+        'AES-256-CFB' => 32,
66
+        'AES-128-CFB' => 16,
67
+    ];
68
+    // one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
69
+    public const DEFAULT_CIPHER = 'AES-256-CTR';
70
+    // default cipher from old Nextcloud versions
71
+    public const LEGACY_CIPHER = 'AES-128-CFB';
72
+
73
+    public const SUPPORTED_KEY_FORMATS = ['hash', 'password'];
74
+    // one out of SUPPORTED_KEY_FORMATS
75
+    public const DEFAULT_KEY_FORMAT = 'hash';
76
+    // default key format, old Nextcloud version encrypted the private key directly
77
+    // with the user password
78
+    public const LEGACY_KEY_FORMAT = 'password';
79
+
80
+    public const HEADER_START = 'HBEGIN';
81
+    public const HEADER_END = 'HEND';
82
+
83
+    // default encoding format, old Nextcloud versions used base64
84
+    public const BINARY_ENCODING_FORMAT = 'binary';
85
+
86
+    /** @var ILogger */
87
+    private $logger;
88
+
89
+    /** @var string */
90
+    private $user;
91
+
92
+    /** @var IConfig */
93
+    private $config;
94
+
95
+    /** @var IL10N */
96
+    private $l;
97
+
98
+    /** @var string|null */
99
+    private $currentCipher;
100
+
101
+    /** @var bool */
102
+    private $supportLegacy;
103
+
104
+    /**
105
+     * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
106
+     */
107
+    private bool $useLegacyBase64Encoding;
108
+
109
+    /**
110
+     * @param ILogger $logger
111
+     * @param IUserSession $userSession
112
+     * @param IConfig $config
113
+     * @param IL10N $l
114
+     */
115
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
116
+        $this->logger = $logger;
117
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
118
+        $this->config = $config;
119
+        $this->l = $l;
120
+        $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
121
+        $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
122
+    }
123
+
124
+    /**
125
+     * create new private/public key-pair for user
126
+     *
127
+     * @return array|bool
128
+     */
129
+    public function createKeyPair() {
130
+        $log = $this->logger;
131
+        $res = $this->getOpenSSLPKey();
132
+
133
+        if (!$res) {
134
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
135
+                ['app' => 'encryption']);
136
+
137
+            if (openssl_error_string()) {
138
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
139
+                    ['app' => 'encryption']);
140
+            }
141
+        } elseif (openssl_pkey_export($res,
142
+            $privateKey,
143
+            null,
144
+            $this->getOpenSSLConfig())) {
145
+            $keyDetails = openssl_pkey_get_details($res);
146
+            $publicKey = $keyDetails['key'];
147
+
148
+            return [
149
+                'publicKey' => $publicKey,
150
+                'privateKey' => $privateKey
151
+            ];
152
+        }
153
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
154
+            ['app' => 'encryption']);
155
+        if (openssl_error_string()) {
156
+            $log->error('Encryption Library:' . openssl_error_string(),
157
+                ['app' => 'encryption']);
158
+        }
159
+
160
+        return false;
161
+    }
162
+
163
+    /**
164
+     * Generates a new private key
165
+     *
166
+     * @return \OpenSSLAsymmetricKey|false
167
+     */
168
+    public function getOpenSSLPKey() {
169
+        $config = $this->getOpenSSLConfig();
170
+        return openssl_pkey_new($config);
171
+    }
172
+
173
+    /**
174
+     * get openSSL Config
175
+     *
176
+     * @return array
177
+     */
178
+    private function getOpenSSLConfig() {
179
+        $config = ['private_key_bits' => 4096];
180
+        $config = array_merge(
181
+            $config,
182
+            $this->config->getSystemValue('openssl', [])
183
+        );
184
+        return $config;
185
+    }
186
+
187
+    /**
188
+     * @throws EncryptionFailedException
189
+     */
190
+    public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false {
191
+        if (!$plainContent) {
192
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
193
+                ['app' => 'encryption']);
194
+            return false;
195
+        }
196
+
197
+        $iv = $this->generateIv();
198
+
199
+        $encryptedContent = $this->encrypt($plainContent,
200
+            $iv,
201
+            $passPhrase,
202
+            $this->getCipher());
203
+
204
+        // Create a signature based on the key as well as the current version
205
+        $sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
206
+
207
+        // combine content to encrypt the IV identifier and actual IV
208
+        $catFile = $this->concatIV($encryptedContent, $iv);
209
+        $catFile = $this->concatSig($catFile, $sig);
210
+        return $this->addPadding($catFile);
211
+    }
212
+
213
+    /**
214
+     * generate header for encrypted file
215
+     *
216
+     * @param string $keyFormat see SUPPORTED_KEY_FORMATS
217
+     * @return string
218
+     * @throws \InvalidArgumentException
219
+     */
220
+    public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
221
+        if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
222
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
223
+        }
224
+
225
+        $header = self::HEADER_START
226
+            . ':cipher:' . $this->getCipher()
227
+            . ':keyFormat:' . $keyFormat;
228
+
229
+        if ($this->useLegacyBase64Encoding !== true) {
230
+            $header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
231
+        }
232
+
233
+        $header .= ':' . self::HEADER_END;
234
+
235
+        return $header;
236
+    }
237
+
238
+    /**
239
+     * @param string $plainContent
240
+     * @param string $iv
241
+     * @param string $passPhrase
242
+     * @param string $cipher
243
+     * @return string
244
+     * @throws EncryptionFailedException
245
+     */
246
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
247
+        $options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
248
+        $encryptedContent = openssl_encrypt($plainContent,
249
+            $cipher,
250
+            $passPhrase,
251
+            $options,
252
+            $iv);
253
+
254
+        if (!$encryptedContent) {
255
+            $error = 'Encryption (symmetric) of content failed';
256
+            $this->logger->error($error . openssl_error_string(),
257
+                ['app' => 'encryption']);
258
+            throw new EncryptionFailedException($error);
259
+        }
260
+
261
+        return $encryptedContent;
262
+    }
263
+
264
+    /**
265
+     * return cipher either from config.php or the default cipher defined in
266
+     * this class
267
+     *
268
+     * @return string
269
+     */
270
+    private function getCachedCipher() {
271
+        if (isset($this->currentCipher)) {
272
+            return $this->currentCipher;
273
+        }
274
+
275
+        // Get cipher either from config.php or the default cipher defined in this class
276
+        $cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
277
+        if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
278
+            $this->logger->warning(
279
+                sprintf(
280
+                    'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
281
+                    $cipher,
282
+                    self::DEFAULT_CIPHER
283
+                ),
284
+                ['app' => 'encryption']
285
+            );
286
+            $cipher = self::DEFAULT_CIPHER;
287
+        }
288
+
289
+        // Remember current cipher to avoid frequent lookups
290
+        $this->currentCipher = $cipher;
291
+        return $this->currentCipher;
292
+    }
293
+
294
+    /**
295
+     * return current encryption cipher
296
+     *
297
+     * @return string
298
+     */
299
+    public function getCipher() {
300
+        return $this->getCachedCipher();
301
+    }
302
+
303
+    /**
304
+     * get key size depending on the cipher
305
+     *
306
+     * @param string $cipher
307
+     * @return int
308
+     * @throws \InvalidArgumentException
309
+     */
310
+    protected function getKeySize($cipher) {
311
+        if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
312
+            return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
313
+        }
314
+
315
+        throw new \InvalidArgumentException(
316
+            sprintf(
317
+                'Unsupported cipher (%s) defined.',
318
+                $cipher
319
+            )
320
+        );
321
+    }
322
+
323
+    /**
324
+     * get legacy cipher
325
+     *
326
+     * @return string
327
+     */
328
+    public function getLegacyCipher() {
329
+        if (!$this->supportLegacy) {
330
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
331
+        }
332
+
333
+        return self::LEGACY_CIPHER;
334
+    }
335
+
336
+    /**
337
+     * @param string $encryptedContent
338
+     * @param string $iv
339
+     * @return string
340
+     */
341
+    private function concatIV($encryptedContent, $iv) {
342
+        return $encryptedContent . '00iv00' . $iv;
343
+    }
344
+
345
+    /**
346
+     * @param string $encryptedContent
347
+     * @param string $signature
348
+     * @return string
349
+     */
350
+    private function concatSig($encryptedContent, $signature) {
351
+        return $encryptedContent . '00sig00' . $signature;
352
+    }
353
+
354
+    /**
355
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
356
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
357
+     * encrypted content and is not used in any crypto primitive.
358
+     *
359
+     * @param string $data
360
+     * @return string
361
+     */
362
+    private function addPadding($data) {
363
+        return $data . 'xxx';
364
+    }
365
+
366
+    /**
367
+     * generate password hash used to encrypt the users private key
368
+     *
369
+     * @param string $password
370
+     * @param string $cipher
371
+     * @param string $uid only used for user keys
372
+     * @return string
373
+     */
374
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
375
+        $instanceId = $this->config->getSystemValue('instanceid');
376
+        $instanceSecret = $this->config->getSystemValue('secret');
377
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
378
+        $keySize = $this->getKeySize($cipher);
379
+
380
+        $hash = hash_pbkdf2(
381
+            'sha256',
382
+            $password,
383
+            $salt,
384
+            100000,
385
+            $keySize,
386
+            true
387
+        );
388
+
389
+        return $hash;
390
+    }
391
+
392
+    /**
393
+     * encrypt private key
394
+     *
395
+     * @param string $privateKey
396
+     * @param string $password
397
+     * @param string $uid for regular users, empty for system keys
398
+     * @return false|string
399
+     */
400
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
401
+        $cipher = $this->getCipher();
402
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
403
+        $encryptedKey = $this->symmetricEncryptFileContent(
404
+            $privateKey,
405
+            $hash,
406
+            0,
407
+            '0'
408
+        );
409
+
410
+        return $encryptedKey;
411
+    }
412
+
413
+    /**
414
+     * @param string $privateKey
415
+     * @param string $password
416
+     * @param string $uid for regular users, empty for system keys
417
+     * @return false|string
418
+     */
419
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
420
+        $header = $this->parseHeader($privateKey);
421
+
422
+        if (isset($header['cipher'])) {
423
+            $cipher = $header['cipher'];
424
+        } else {
425
+            $cipher = $this->getLegacyCipher();
426
+        }
427
+
428
+        if (isset($header['keyFormat'])) {
429
+            $keyFormat = $header['keyFormat'];
430
+        } else {
431
+            $keyFormat = self::LEGACY_KEY_FORMAT;
432
+        }
433
+
434
+        if ($keyFormat === self::DEFAULT_KEY_FORMAT) {
435
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
436
+        }
437
+
438
+        $binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
439
+
440
+        // If we found a header we need to remove it from the key we want to decrypt
441
+        if (!empty($header)) {
442
+            $privateKey = substr($privateKey,
443
+                strpos($privateKey,
444
+                    self::HEADER_END) + strlen(self::HEADER_END));
445
+        }
446
+
447
+        $plainKey = $this->symmetricDecryptFileContent(
448
+            $privateKey,
449
+            $password,
450
+            $cipher,
451
+            0,
452
+            0,
453
+            $binaryEncoding
454
+        );
455
+
456
+        if ($this->isValidPrivateKey($plainKey) === false) {
457
+            return false;
458
+        }
459
+
460
+        return $plainKey;
461
+    }
462
+
463
+    /**
464
+     * check if it is a valid private key
465
+     *
466
+     * @param string $plainKey
467
+     * @return bool
468
+     */
469
+    protected function isValidPrivateKey($plainKey) {
470
+        $res = openssl_get_privatekey($plainKey);
471
+        if (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey') {
472
+            $sslInfo = openssl_pkey_get_details($res);
473
+            if (isset($sslInfo['key'])) {
474
+                return true;
475
+            }
476
+        }
477
+
478
+        return false;
479
+    }
480
+
481
+    /**
482
+     * @param string $keyFileContents
483
+     * @param string $passPhrase
484
+     * @param string $cipher
485
+     * @param int $version
486
+     * @param int|string $position
487
+     * @param boolean $binaryEncoding
488
+     * @return string
489
+     * @throws DecryptionFailedException
490
+     */
491
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
492
+        if ($keyFileContents == '') {
493
+            return '';
494
+        }
495
+
496
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
497
+
498
+        if ($catFile['signature'] !== false) {
499
+            try {
500
+                // First try the new format
501
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
502
+            } catch (GenericEncryptionException $e) {
503
+                // For compatibility with old files check the version without _
504
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
505
+            }
506
+        }
507
+
508
+        return $this->decrypt($catFile['encrypted'],
509
+            $catFile['iv'],
510
+            $passPhrase,
511
+            $cipher,
512
+            $binaryEncoding);
513
+    }
514
+
515
+    /**
516
+     * check for valid signature
517
+     *
518
+     * @throws GenericEncryptionException
519
+     */
520
+    private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void {
521
+        $enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
522
+
523
+        $signature = $this->createSignature($data, $passPhrase);
524
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
525
+
526
+        if (!$isCorrectHash) {
527
+            if ($enforceSignature) {
528
+                throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
529
+            } else {
530
+                $this->logger->info("Signature check skipped", ['app' => 'encryption']);
531
+            }
532
+        }
533
+    }
534
+
535
+    /**
536
+     * create signature
537
+     */
538
+    private function createSignature(string $data, string $passPhrase): string {
539
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
540
+        return hash_hmac('sha256', $data, $passPhrase);
541
+    }
542
+
543
+
544
+    /**
545
+     * remove padding
546
+     *
547
+     * @param string $padded
548
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
549
+     * @return string|false
550
+     */
551
+    private function removePadding($padded, $hasSignature = false) {
552
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
553
+            return substr($padded, 0, -2);
554
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
555
+            return substr($padded, 0, -3);
556
+        }
557
+        return false;
558
+    }
559
+
560
+    /**
561
+     * split meta data from encrypted file
562
+     * Note: for now, we assume that the meta data always start with the iv
563
+     *       followed by the signature, if available
564
+     *
565
+     * @param string $catFile
566
+     * @param string $cipher
567
+     * @return array
568
+     */
569
+    private function splitMetaData($catFile, $cipher) {
570
+        if ($this->hasSignature($catFile, $cipher)) {
571
+            $catFile = $this->removePadding($catFile, true);
572
+            $meta = substr($catFile, -93);
573
+            $iv = substr($meta, strlen('00iv00'), 16);
574
+            $sig = substr($meta, 22 + strlen('00sig00'));
575
+            $encrypted = substr($catFile, 0, -93);
576
+        } else {
577
+            $catFile = $this->removePadding($catFile);
578
+            $meta = substr($catFile, -22);
579
+            $iv = substr($meta, -16);
580
+            $sig = false;
581
+            $encrypted = substr($catFile, 0, -22);
582
+        }
583
+
584
+        return [
585
+            'encrypted' => $encrypted,
586
+            'iv' => $iv,
587
+            'signature' => $sig
588
+        ];
589
+    }
590
+
591
+    /**
592
+     * check if encrypted block is signed
593
+     *
594
+     * @param string $catFile
595
+     * @param string $cipher
596
+     * @return bool
597
+     * @throws GenericEncryptionException
598
+     */
599
+    private function hasSignature($catFile, $cipher) {
600
+        $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
601
+
602
+        $meta = substr($catFile, -93);
603
+        $signaturePosition = strpos($meta, '00sig00');
604
+
605
+        // If we no longer support the legacy format then everything needs a signature
606
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
607
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
608
+        }
609
+
610
+        // Enforce signature for the new 'CTR' ciphers
611
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
612
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
613
+        }
614
+
615
+        return ($signaturePosition !== false);
616
+    }
617
+
618
+
619
+    /**
620
+     * @param string $encryptedContent
621
+     * @param string $iv
622
+     * @param string $passPhrase
623
+     * @param string $cipher
624
+     * @param boolean $binaryEncoding
625
+     * @return string
626
+     * @throws DecryptionFailedException
627
+     */
628
+    private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
629
+        $options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
630
+        $plainContent = openssl_decrypt($encryptedContent,
631
+            $cipher,
632
+            $passPhrase,
633
+            $options,
634
+            $iv);
635
+
636
+        if ($plainContent) {
637
+            return $plainContent;
638
+        } else {
639
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
640
+        }
641
+    }
642
+
643
+    /**
644
+     * @param string $data
645
+     * @return array
646
+     */
647
+    protected function parseHeader($data) {
648
+        $result = [];
649
+
650
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
651
+            $endAt = strpos($data, self::HEADER_END);
652
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
653
+
654
+            // +1 not to start with an ':' which would result in empty element at the beginning
655
+            $exploded = explode(':',
656
+                substr($header, strlen(self::HEADER_START) + 1));
657
+
658
+            $element = array_shift($exploded);
659
+
660
+            while ($element !== self::HEADER_END) {
661
+                $result[$element] = array_shift($exploded);
662
+                $element = array_shift($exploded);
663
+            }
664
+        }
665
+
666
+        return $result;
667
+    }
668
+
669
+    /**
670
+     * generate initialization vector
671
+     *
672
+     * @return string
673
+     * @throws GenericEncryptionException
674
+     */
675
+    private function generateIv() {
676
+        return random_bytes(16);
677
+    }
678
+
679
+    /**
680
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
681
+     * as file key
682
+     *
683
+     * @return string
684
+     * @throws \Exception
685
+     */
686
+    public function generateFileKey() {
687
+        return random_bytes(32);
688
+    }
689
+
690
+    /**
691
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
692
+     * @throws MultiKeyDecryptException
693
+     */
694
+    public function multiKeyDecrypt(string $shareKey, $privateKey): string {
695
+        $plainContent = '';
696
+
697
+        // decrypt the intermediate key with RSA
698
+        if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) {
699
+            return $intermediate;
700
+        } else {
701
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
702
+        }
703
+    }
704
+
705
+    /**
706
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey
707
+     * @throws MultiKeyDecryptException
708
+     */
709
+    public function multiKeyDecryptLegacy(string $encKeyFile, string $shareKey, $privateKey): string {
710
+        if (!$encKeyFile) {
711
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
712
+        }
713
+
714
+        $plainContent = '';
715
+        if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
716
+            return $plainContent;
717
+        } else {
718
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
719
+        }
720
+    }
721
+
722
+    /**
723
+     * @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string> $keyFiles
724
+     * @throws MultiKeyEncryptException
725
+     */
726
+    public function multiKeyEncrypt(string $plainContent, array $keyFiles): array {
727
+        if (empty($plainContent)) {
728
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
729
+        }
730
+
731
+        // Set empty vars to be set by openssl by reference
732
+        $shareKeys = [];
733
+        $mappedShareKeys = [];
734
+
735
+        // make sure that there is at least one public key to use
736
+        if (count($keyFiles) >= 1) {
737
+            // prepare the encrypted keys
738
+            $shareKeys = [];
739
+
740
+            // iterate over the public keys and encrypt the intermediate
741
+            // for each of them with RSA
742
+            foreach ($keyFiles as $tmp_key) {
743
+                if (openssl_public_encrypt($plainContent, $tmp_output, $tmp_key, OPENSSL_PKCS1_OAEP_PADDING)) {
744
+                    $shareKeys[] = $tmp_output;
745
+                }
746
+            }
747
+
748
+            // set the result if everything worked fine
749
+            if (count($keyFiles) === count($shareKeys)) {
750
+                $i = 0;
751
+
752
+                // Ensure each shareKey is labelled with its corresponding key id
753
+                foreach ($keyFiles as $userId => $publicKey) {
754
+                    $mappedShareKeys[$userId] = $shareKeys[$i];
755
+                    $i++;
756
+                }
757
+
758
+                return $mappedShareKeys;
759
+            }
760
+        }
761
+        throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
762
+    }
763
+
764
+    /**
765
+     * @param string $plainContent
766
+     * @param array $keyFiles
767
+     * @return array
768
+     * @throws MultiKeyEncryptException
769
+     * @deprecated 27.0.0 use multiKeyEncrypt
770
+     */
771
+    public function multiKeyEncryptLegacy($plainContent, array $keyFiles) {
772
+        // openssl_seal returns false without errors if plaincontent is empty
773
+        // so trigger our own error
774
+        if (empty($plainContent)) {
775
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
776
+        }
777
+
778
+        // Set empty vars to be set by openssl by reference
779
+        $sealed = '';
780
+        $shareKeys = [];
781
+        $mappedShareKeys = [];
782
+
783
+        if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
784
+            $i = 0;
785
+
786
+            // Ensure each shareKey is labelled with its corresponding key id
787
+            foreach ($keyFiles as $userId => $publicKey) {
788
+                $mappedShareKeys[$userId] = $shareKeys[$i];
789
+                $i++;
790
+            }
791
+
792
+            return [
793
+                'keys' => $mappedShareKeys,
794
+                'data' => $sealed
795
+            ];
796
+        } else {
797
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
798
+        }
799
+    }
800
+
801
+    /**
802
+     * returns the value of $useLegacyBase64Encoding
803
+     *
804
+     * @return bool
805
+     */
806
+    public function useLegacyBase64Encoding(): bool {
807
+        return $this->useLegacyBase64Encoding;
808
+    }
809
+
810
+    /**
811
+     * Uses phpseclib RC4 implementation
812
+     */
813
+    private function rc4Decrypt(string $data, string $secret): string {
814
+        $rc4 = new RC4();
815
+        /** @psalm-suppress InternalMethod */
816
+        $rc4->setKey($secret);
817
+
818
+        return $rc4->decrypt($data);
819
+    }
820
+
821
+    /**
822
+     * Uses phpseclib RC4 implementation
823
+     */
824
+    private function rc4Encrypt(string $data, string $secret): string {
825
+        $rc4 = new RC4();
826
+        /** @psalm-suppress InternalMethod */
827
+        $rc4->setKey($secret);
828
+
829
+        return $rc4->encrypt($data);
830
+    }
831
+
832
+    /**
833
+     * Custom implementation of openssl_open()
834
+     *
835
+     * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key
836
+     * @throws DecryptionFailedException
837
+     */
838
+    private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool {
839
+        $result = false;
840
+
841
+        // check if RC4 is used
842
+        if (strcasecmp($cipher_algo, "rc4") === 0) {
843
+            // decrypt the intermediate key with RSA
844
+            if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) {
845
+                // decrypt the file key with the intermediate key
846
+                // using our own RC4 implementation
847
+                $output = $this->rc4Decrypt($data, $intermediate);
848
+                $result = (strlen($output) === strlen($data));
849
+            }
850
+        } else {
851
+            throw new DecryptionFailedException('Unsupported cipher '.$cipher_algo);
852
+        }
853
+
854
+        return $result;
855
+    }
856
+
857
+    /**
858
+     * Custom implementation of openssl_seal()
859
+     *
860
+     * @deprecated 27.0.0 use multiKeyEncrypt
861
+     * @throws EncryptionFailedException
862
+     */
863
+    private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false {
864
+        $result = false;
865
+
866
+        // check if RC4 is used
867
+        if (strcasecmp($cipher_algo, "rc4") === 0) {
868
+            // make sure that there is at least one public key to use
869
+            if (count($public_key) >= 1) {
870
+                // generate the intermediate key
871
+                $intermediate = openssl_random_pseudo_bytes(16, $strong_result);
872
+
873
+                // check if we got strong random data
874
+                if ($strong_result) {
875
+                    // encrypt the file key with the intermediate key
876
+                    // using our own RC4 implementation
877
+                    $sealed_data = $this->rc4Encrypt($data, $intermediate);
878
+                    if (strlen($sealed_data) === strlen($data)) {
879
+                        // prepare the encrypted keys
880
+                        $encrypted_keys = [];
881
+
882
+                        // iterate over the public keys and encrypt the intermediate
883
+                        // for each of them with RSA
884
+                        foreach ($public_key as $tmp_key) {
885
+                            if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) {
886
+                                $encrypted_keys[] = $tmp_output;
887
+                            }
888
+                        }
889
+
890
+                        // set the result if everything worked fine
891
+                        if (count($public_key) === count($encrypted_keys)) {
892
+                            $result = strlen($sealed_data);
893
+                        }
894
+                    }
895
+                }
896
+            }
897
+        } else {
898
+            throw new EncryptionFailedException('Unsupported cipher '.$cipher_algo);
899
+        }
900
+
901
+        return $result;
902
+    }
903 903
 }
Please login to merge, or discard this patch.