Completed
Pull Request — master (#8059)
by Morris
83:09 queued 68:01
created
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +636 added lines, -636 removed lines patch added patch discarded remove patch
@@ -55,641 +55,641 @@
 block discarded – undo
55 55
  */
56 56
 class Crypt {
57 57
 
58
-	const DEFAULT_CIPHER = 'AES-256-CTR';
59
-	// default cipher from old Nextcloud versions
60
-	const LEGACY_CIPHER = 'AES-128-CFB';
61
-
62
-	// default key format, old Nextcloud version encrypted the private key directly
63
-	// with the user password
64
-	const LEGACY_KEY_FORMAT = 'password';
65
-
66
-	const HEADER_START = 'HBEGIN';
67
-	const HEADER_END = 'HEND';
68
-
69
-	/** @var ILogger */
70
-	private $logger;
71
-
72
-	/** @var string */
73
-	private $user;
74
-
75
-	/** @var IConfig */
76
-	private $config;
77
-
78
-	/** @var array */
79
-	private $supportedKeyFormats;
80
-
81
-	/** @var IL10N */
82
-	private $l;
83
-
84
-	/** @var array */
85
-	private $supportedCiphersAndKeySize = [
86
-		'AES-256-CTR' => 32,
87
-		'AES-128-CTR' => 16,
88
-		'AES-256-CFB' => 32,
89
-		'AES-128-CFB' => 16,
90
-	];
91
-
92
-	/**
93
-	 * @param ILogger $logger
94
-	 * @param IUserSession $userSession
95
-	 * @param IConfig $config
96
-	 * @param IL10N $l
97
-	 */
98
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
99
-		$this->logger = $logger;
100
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
101
-		$this->config = $config;
102
-		$this->l = $l;
103
-		$this->supportedKeyFormats = ['hash', 'password'];
104
-	}
105
-
106
-	/**
107
-	 * create new private/public key-pair for user
108
-	 *
109
-	 * @return array|bool
110
-	 */
111
-	public function createKeyPair() {
112
-
113
-		$log = $this->logger;
114
-		$res = $this->getOpenSSLPKey();
115
-
116
-		if (!$res) {
117
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
118
-				['app' => 'encryption']);
119
-
120
-			if (openssl_error_string()) {
121
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
122
-					['app' => 'encryption']);
123
-			}
124
-		} elseif (openssl_pkey_export($res,
125
-			$privateKey,
126
-			null,
127
-			$this->getOpenSSLConfig())) {
128
-			$keyDetails = openssl_pkey_get_details($res);
129
-			$publicKey = $keyDetails['key'];
130
-
131
-			return [
132
-				'publicKey' => $publicKey,
133
-				'privateKey' => $privateKey
134
-			];
135
-		}
136
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
137
-			['app' => 'encryption']);
138
-		if (openssl_error_string()) {
139
-			$log->error('Encryption Library:' . openssl_error_string(),
140
-				['app' => 'encryption']);
141
-		}
142
-
143
-		return false;
144
-	}
145
-
146
-	/**
147
-	 * Generates a new private key
148
-	 *
149
-	 * @return resource
150
-	 */
151
-	public function getOpenSSLPKey() {
152
-		$config = $this->getOpenSSLConfig();
153
-		return openssl_pkey_new($config);
154
-	}
155
-
156
-	/**
157
-	 * get openSSL Config
158
-	 *
159
-	 * @return array
160
-	 */
161
-	private function getOpenSSLConfig() {
162
-		$config = ['private_key_bits' => 4096];
163
-		$config = array_merge(
164
-			$config,
165
-			$this->config->getSystemValue('openssl', [])
166
-		);
167
-		return $config;
168
-	}
169
-
170
-	/**
171
-	 * @param string $plainContent
172
-	 * @param string $passPhrase
173
-	 * @param int $version
174
-	 * @param int $position
175
-	 * @return false|string
176
-	 * @throws EncryptionFailedException
177
-	 */
178
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
179
-
180
-		if (!$plainContent) {
181
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
182
-				['app' => 'encryption']);
183
-			return false;
184
-		}
185
-
186
-		$iv = $this->generateIv();
187
-
188
-		$encryptedContent = $this->encrypt($plainContent,
189
-			$iv,
190
-			$passPhrase,
191
-			$this->getCipher());
192
-
193
-		// Create a signature based on the key as well as the current version
194
-		$sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
195
-
196
-		// combine content to encrypt the IV identifier and actual IV
197
-		$catFile = $this->concatIV($encryptedContent, $iv);
198
-		$catFile = $this->concatSig($catFile, $sig);
199
-		$padded = $this->addPadding($catFile);
200
-
201
-		return $padded;
202
-	}
203
-
204
-	/**
205
-	 * generate header for encrypted file
206
-	 *
207
-	 * @param string $keyFormat (can be 'hash' or 'password')
208
-	 * @return string
209
-	 * @throws \InvalidArgumentException
210
-	 */
211
-	public function generateHeader($keyFormat = 'hash') {
212
-
213
-		if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
214
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
215
-		}
216
-
217
-		$cipher = $this->getCipher();
218
-
219
-		$header = self::HEADER_START
220
-			. ':cipher:' . $cipher
221
-			. ':keyFormat:' . $keyFormat
222
-			. ':' . self::HEADER_END;
223
-
224
-		return $header;
225
-	}
226
-
227
-	/**
228
-	 * @param string $plainContent
229
-	 * @param string $iv
230
-	 * @param string $passPhrase
231
-	 * @param string $cipher
232
-	 * @return string
233
-	 * @throws EncryptionFailedException
234
-	 */
235
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
236
-		$encryptedContent = openssl_encrypt($plainContent,
237
-			$cipher,
238
-			$passPhrase,
239
-			false,
240
-			$iv);
241
-
242
-		if (!$encryptedContent) {
243
-			$error = 'Encryption (symmetric) of content failed';
244
-			$this->logger->error($error . openssl_error_string(),
245
-				['app' => 'encryption']);
246
-			throw new EncryptionFailedException($error);
247
-		}
248
-
249
-		return $encryptedContent;
250
-	}
251
-
252
-	/**
253
-	 * return Cipher either from config.php or the default cipher defined in
254
-	 * this class
255
-	 *
256
-	 * @return string
257
-	 */
258
-	public function getCipher() {
259
-		$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
260
-		if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
261
-			$this->logger->warning(
262
-					sprintf(
263
-							'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
264
-							$cipher,
265
-							self::DEFAULT_CIPHER
266
-					),
267
-				['app' => 'encryption']);
268
-			$cipher = self::DEFAULT_CIPHER;
269
-		}
270
-
271
-		// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
272
-		if(OPENSSL_VERSION_NUMBER < 0x1000101f) {
273
-			if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
274
-				$cipher = self::LEGACY_CIPHER;
275
-			}
276
-		}
277
-
278
-		return $cipher;
279
-	}
280
-
281
-	/**
282
-	 * get key size depending on the cipher
283
-	 *
284
-	 * @param string $cipher
285
-	 * @return int
286
-	 * @throws \InvalidArgumentException
287
-	 */
288
-	protected function getKeySize($cipher) {
289
-		if(isset($this->supportedCiphersAndKeySize[$cipher])) {
290
-			return $this->supportedCiphersAndKeySize[$cipher];
291
-		}
292
-
293
-		throw new \InvalidArgumentException(
294
-			sprintf(
295
-					'Unsupported cipher (%s) defined.',
296
-					$cipher
297
-			)
298
-		);
299
-	}
300
-
301
-	/**
302
-	 * get legacy cipher
303
-	 *
304
-	 * @return string
305
-	 */
306
-	public function getLegacyCipher() {
307
-		return self::LEGACY_CIPHER;
308
-	}
309
-
310
-	/**
311
-	 * @param string $encryptedContent
312
-	 * @param string $iv
313
-	 * @return string
314
-	 */
315
-	private function concatIV($encryptedContent, $iv) {
316
-		return $encryptedContent . '00iv00' . $iv;
317
-	}
318
-
319
-	/**
320
-	 * @param string $encryptedContent
321
-	 * @param string $signature
322
-	 * @return string
323
-	 */
324
-	private function concatSig($encryptedContent, $signature) {
325
-		return $encryptedContent . '00sig00' . $signature;
326
-	}
327
-
328
-	/**
329
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
330
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
331
-	 * encrypted content and is not used in any crypto primitive.
332
-	 *
333
-	 * @param string $data
334
-	 * @return string
335
-	 */
336
-	private function addPadding($data) {
337
-		return $data . 'xxx';
338
-	}
339
-
340
-	/**
341
-	 * generate password hash used to encrypt the users private key
342
-	 *
343
-	 * @param string $password
344
-	 * @param string $cipher
345
-	 * @param string $uid only used for user keys
346
-	 * @return string
347
-	 */
348
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
349
-		$instanceId = $this->config->getSystemValue('instanceid');
350
-		$instanceSecret = $this->config->getSystemValue('secret');
351
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
352
-		$keySize = $this->getKeySize($cipher);
353
-
354
-		$hash = hash_pbkdf2(
355
-			'sha256',
356
-			$password,
357
-			$salt,
358
-			100000,
359
-			$keySize,
360
-			true
361
-		);
362
-
363
-		return $hash;
364
-	}
365
-
366
-	/**
367
-	 * encrypt private key
368
-	 *
369
-	 * @param string $privateKey
370
-	 * @param string $password
371
-	 * @param string $uid for regular users, empty for system keys
372
-	 * @return false|string
373
-	 */
374
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
375
-		$cipher = $this->getCipher();
376
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
377
-		$encryptedKey = $this->symmetricEncryptFileContent(
378
-			$privateKey,
379
-			$hash,
380
-			0,
381
-			0
382
-		);
383
-
384
-		return $encryptedKey;
385
-	}
386
-
387
-	/**
388
-	 * @param string $privateKey
389
-	 * @param string $password
390
-	 * @param string $uid for regular users, empty for system keys
391
-	 * @return false|string
392
-	 */
393
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
394
-
395
-		$header = $this->parseHeader($privateKey);
396
-
397
-		if (isset($header['cipher'])) {
398
-			$cipher = $header['cipher'];
399
-		} else {
400
-			$cipher = self::LEGACY_CIPHER;
401
-		}
402
-
403
-		if (isset($header['keyFormat'])) {
404
-			$keyFormat = $header['keyFormat'];
405
-		} else {
406
-			$keyFormat = self::LEGACY_KEY_FORMAT;
407
-		}
408
-
409
-		if ($keyFormat === 'hash') {
410
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
411
-		}
412
-
413
-		// If we found a header we need to remove it from the key we want to decrypt
414
-		if (!empty($header)) {
415
-			$privateKey = substr($privateKey,
416
-				strpos($privateKey,
417
-					self::HEADER_END) + strlen(self::HEADER_END));
418
-		}
419
-
420
-		$plainKey = $this->symmetricDecryptFileContent(
421
-			$privateKey,
422
-			$password,
423
-			$cipher,
424
-			0
425
-		);
426
-
427
-		if ($this->isValidPrivateKey($plainKey) === false) {
428
-			return false;
429
-		}
430
-
431
-		return $plainKey;
432
-	}
433
-
434
-	/**
435
-	 * check if it is a valid private key
436
-	 *
437
-	 * @param string $plainKey
438
-	 * @return bool
439
-	 */
440
-	protected function isValidPrivateKey($plainKey) {
441
-		$res = openssl_get_privatekey($plainKey);
442
-		if (is_resource($res)) {
443
-			$sslInfo = openssl_pkey_get_details($res);
444
-			if (isset($sslInfo['key'])) {
445
-				return true;
446
-			}
447
-		}
448
-
449
-		return false;
450
-	}
451
-
452
-	/**
453
-	 * @param string $keyFileContents
454
-	 * @param string $passPhrase
455
-	 * @param string $cipher
456
-	 * @param int $version
457
-	 * @param int $position
458
-	 * @return string
459
-	 * @throws DecryptionFailedException
460
-	 */
461
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
462
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
463
-
464
-		if ($catFile['signature'] !== false) {
465
-			$this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
466
-		}
467
-
468
-		return $this->decrypt($catFile['encrypted'],
469
-			$catFile['iv'],
470
-			$passPhrase,
471
-			$cipher);
472
-	}
473
-
474
-	/**
475
-	 * check for valid signature
476
-	 *
477
-	 * @param string $data
478
-	 * @param string $passPhrase
479
-	 * @param string $expectedSignature
480
-	 * @throws GenericEncryptionException
481
-	 */
482
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
483
-		$signature = $this->createSignature($data, $passPhrase);
484
-		if (!hash_equals($expectedSignature, $signature)) {
485
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
486
-		}
487
-	}
488
-
489
-	/**
490
-	 * create signature
491
-	 *
492
-	 * @param string $data
493
-	 * @param string $passPhrase
494
-	 * @return string
495
-	 */
496
-	private function createSignature($data, $passPhrase) {
497
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
498
-		$signature = hash_hmac('sha256', $data, $passPhrase);
499
-		return $signature;
500
-	}
501
-
502
-
503
-	/**
504
-	 * remove padding
505
-	 *
506
-	 * @param string $padded
507
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
508
-	 * @return string|false
509
-	 */
510
-	private function removePadding($padded, $hasSignature = false) {
511
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
512
-			return substr($padded, 0, -2);
513
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
514
-			return substr($padded, 0, -3);
515
-		}
516
-		return false;
517
-	}
518
-
519
-	/**
520
-	 * split meta data from encrypted file
521
-	 * Note: for now, we assume that the meta data always start with the iv
522
-	 *       followed by the signature, if available
523
-	 *
524
-	 * @param string $catFile
525
-	 * @param string $cipher
526
-	 * @return array
527
-	 */
528
-	private function splitMetaData($catFile, $cipher) {
529
-		if ($this->hasSignature($catFile, $cipher)) {
530
-			$catFile = $this->removePadding($catFile, true);
531
-			$meta = substr($catFile, -93);
532
-			$iv = substr($meta, strlen('00iv00'), 16);
533
-			$sig = substr($meta, 22 + strlen('00sig00'));
534
-			$encrypted = substr($catFile, 0, -93);
535
-		} else {
536
-			$catFile = $this->removePadding($catFile);
537
-			$meta = substr($catFile, -22);
538
-			$iv = substr($meta, -16);
539
-			$sig = false;
540
-			$encrypted = substr($catFile, 0, -22);
541
-		}
542
-
543
-		return [
544
-			'encrypted' => $encrypted,
545
-			'iv' => $iv,
546
-			'signature' => $sig
547
-		];
548
-	}
549
-
550
-	/**
551
-	 * check if encrypted block is signed
552
-	 *
553
-	 * @param string $catFile
554
-	 * @param string $cipher
555
-	 * @return bool
556
-	 * @throws GenericEncryptionException
557
-	 */
558
-	private function hasSignature($catFile, $cipher) {
559
-		$meta = substr($catFile, -93);
560
-		$signaturePosition = strpos($meta, '00sig00');
561
-
562
-		// enforce signature for the new 'CTR' ciphers
563
-		if ($signaturePosition === false && stripos($cipher, 'ctr') !== false) {
564
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
565
-		}
566
-
567
-		return ($signaturePosition !== false);
568
-	}
569
-
570
-
571
-	/**
572
-	 * @param string $encryptedContent
573
-	 * @param string $iv
574
-	 * @param string $passPhrase
575
-	 * @param string $cipher
576
-	 * @return string
577
-	 * @throws DecryptionFailedException
578
-	 */
579
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
580
-		$plainContent = openssl_decrypt($encryptedContent,
581
-			$cipher,
582
-			$passPhrase,
583
-			false,
584
-			$iv);
585
-
586
-		if ($plainContent) {
587
-			return $plainContent;
588
-		} else {
589
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
590
-		}
591
-	}
592
-
593
-	/**
594
-	 * @param string $data
595
-	 * @return array
596
-	 */
597
-	protected function parseHeader($data) {
598
-		$result = [];
599
-
600
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
601
-			$endAt = strpos($data, self::HEADER_END);
602
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
603
-
604
-			// +1 not to start with an ':' which would result in empty element at the beginning
605
-			$exploded = explode(':',
606
-				substr($header, strlen(self::HEADER_START) + 1));
607
-
608
-			$element = array_shift($exploded);
609
-
610
-			while ($element !== self::HEADER_END) {
611
-				$result[$element] = array_shift($exploded);
612
-				$element = array_shift($exploded);
613
-			}
614
-		}
615
-
616
-		return $result;
617
-	}
618
-
619
-	/**
620
-	 * generate initialization vector
621
-	 *
622
-	 * @return string
623
-	 * @throws GenericEncryptionException
624
-	 */
625
-	private function generateIv() {
626
-		return random_bytes(16);
627
-	}
628
-
629
-	/**
630
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
631
-	 * as file key
632
-	 *
633
-	 * @return string
634
-	 * @throws \Exception
635
-	 */
636
-	public function generateFileKey() {
637
-		return random_bytes(32);
638
-	}
639
-
640
-	/**
641
-	 * @param $encKeyFile
642
-	 * @param $shareKey
643
-	 * @param $privateKey
644
-	 * @return string
645
-	 * @throws MultiKeyDecryptException
646
-	 */
647
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
648
-		if (!$encKeyFile) {
649
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
650
-		}
651
-
652
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
653
-			return $plainContent;
654
-		} else {
655
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
656
-		}
657
-	}
658
-
659
-	/**
660
-	 * @param string $plainContent
661
-	 * @param array $keyFiles
662
-	 * @return array
663
-	 * @throws MultiKeyEncryptException
664
-	 */
665
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
666
-		// openssl_seal returns false without errors if plaincontent is empty
667
-		// so trigger our own error
668
-		if (empty($plainContent)) {
669
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
670
-		}
671
-
672
-		// Set empty vars to be set by openssl by reference
673
-		$sealed = '';
674
-		$shareKeys = [];
675
-		$mappedShareKeys = [];
676
-
677
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
678
-			$i = 0;
679
-
680
-			// Ensure each shareKey is labelled with its corresponding key id
681
-			foreach ($keyFiles as $userId => $publicKey) {
682
-				$mappedShareKeys[$userId] = $shareKeys[$i];
683
-				$i++;
684
-			}
685
-
686
-			return [
687
-				'keys' => $mappedShareKeys,
688
-				'data' => $sealed
689
-			];
690
-		} else {
691
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
692
-		}
693
-	}
58
+    const DEFAULT_CIPHER = 'AES-256-CTR';
59
+    // default cipher from old Nextcloud versions
60
+    const LEGACY_CIPHER = 'AES-128-CFB';
61
+
62
+    // default key format, old Nextcloud version encrypted the private key directly
63
+    // with the user password
64
+    const LEGACY_KEY_FORMAT = 'password';
65
+
66
+    const HEADER_START = 'HBEGIN';
67
+    const HEADER_END = 'HEND';
68
+
69
+    /** @var ILogger */
70
+    private $logger;
71
+
72
+    /** @var string */
73
+    private $user;
74
+
75
+    /** @var IConfig */
76
+    private $config;
77
+
78
+    /** @var array */
79
+    private $supportedKeyFormats;
80
+
81
+    /** @var IL10N */
82
+    private $l;
83
+
84
+    /** @var array */
85
+    private $supportedCiphersAndKeySize = [
86
+        'AES-256-CTR' => 32,
87
+        'AES-128-CTR' => 16,
88
+        'AES-256-CFB' => 32,
89
+        'AES-128-CFB' => 16,
90
+    ];
91
+
92
+    /**
93
+     * @param ILogger $logger
94
+     * @param IUserSession $userSession
95
+     * @param IConfig $config
96
+     * @param IL10N $l
97
+     */
98
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
99
+        $this->logger = $logger;
100
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
101
+        $this->config = $config;
102
+        $this->l = $l;
103
+        $this->supportedKeyFormats = ['hash', 'password'];
104
+    }
105
+
106
+    /**
107
+     * create new private/public key-pair for user
108
+     *
109
+     * @return array|bool
110
+     */
111
+    public function createKeyPair() {
112
+
113
+        $log = $this->logger;
114
+        $res = $this->getOpenSSLPKey();
115
+
116
+        if (!$res) {
117
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
118
+                ['app' => 'encryption']);
119
+
120
+            if (openssl_error_string()) {
121
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
122
+                    ['app' => 'encryption']);
123
+            }
124
+        } elseif (openssl_pkey_export($res,
125
+            $privateKey,
126
+            null,
127
+            $this->getOpenSSLConfig())) {
128
+            $keyDetails = openssl_pkey_get_details($res);
129
+            $publicKey = $keyDetails['key'];
130
+
131
+            return [
132
+                'publicKey' => $publicKey,
133
+                'privateKey' => $privateKey
134
+            ];
135
+        }
136
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
137
+            ['app' => 'encryption']);
138
+        if (openssl_error_string()) {
139
+            $log->error('Encryption Library:' . openssl_error_string(),
140
+                ['app' => 'encryption']);
141
+        }
142
+
143
+        return false;
144
+    }
145
+
146
+    /**
147
+     * Generates a new private key
148
+     *
149
+     * @return resource
150
+     */
151
+    public function getOpenSSLPKey() {
152
+        $config = $this->getOpenSSLConfig();
153
+        return openssl_pkey_new($config);
154
+    }
155
+
156
+    /**
157
+     * get openSSL Config
158
+     *
159
+     * @return array
160
+     */
161
+    private function getOpenSSLConfig() {
162
+        $config = ['private_key_bits' => 4096];
163
+        $config = array_merge(
164
+            $config,
165
+            $this->config->getSystemValue('openssl', [])
166
+        );
167
+        return $config;
168
+    }
169
+
170
+    /**
171
+     * @param string $plainContent
172
+     * @param string $passPhrase
173
+     * @param int $version
174
+     * @param int $position
175
+     * @return false|string
176
+     * @throws EncryptionFailedException
177
+     */
178
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
179
+
180
+        if (!$plainContent) {
181
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
182
+                ['app' => 'encryption']);
183
+            return false;
184
+        }
185
+
186
+        $iv = $this->generateIv();
187
+
188
+        $encryptedContent = $this->encrypt($plainContent,
189
+            $iv,
190
+            $passPhrase,
191
+            $this->getCipher());
192
+
193
+        // Create a signature based on the key as well as the current version
194
+        $sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
195
+
196
+        // combine content to encrypt the IV identifier and actual IV
197
+        $catFile = $this->concatIV($encryptedContent, $iv);
198
+        $catFile = $this->concatSig($catFile, $sig);
199
+        $padded = $this->addPadding($catFile);
200
+
201
+        return $padded;
202
+    }
203
+
204
+    /**
205
+     * generate header for encrypted file
206
+     *
207
+     * @param string $keyFormat (can be 'hash' or 'password')
208
+     * @return string
209
+     * @throws \InvalidArgumentException
210
+     */
211
+    public function generateHeader($keyFormat = 'hash') {
212
+
213
+        if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
214
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
215
+        }
216
+
217
+        $cipher = $this->getCipher();
218
+
219
+        $header = self::HEADER_START
220
+            . ':cipher:' . $cipher
221
+            . ':keyFormat:' . $keyFormat
222
+            . ':' . self::HEADER_END;
223
+
224
+        return $header;
225
+    }
226
+
227
+    /**
228
+     * @param string $plainContent
229
+     * @param string $iv
230
+     * @param string $passPhrase
231
+     * @param string $cipher
232
+     * @return string
233
+     * @throws EncryptionFailedException
234
+     */
235
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
236
+        $encryptedContent = openssl_encrypt($plainContent,
237
+            $cipher,
238
+            $passPhrase,
239
+            false,
240
+            $iv);
241
+
242
+        if (!$encryptedContent) {
243
+            $error = 'Encryption (symmetric) of content failed';
244
+            $this->logger->error($error . openssl_error_string(),
245
+                ['app' => 'encryption']);
246
+            throw new EncryptionFailedException($error);
247
+        }
248
+
249
+        return $encryptedContent;
250
+    }
251
+
252
+    /**
253
+     * return Cipher either from config.php or the default cipher defined in
254
+     * this class
255
+     *
256
+     * @return string
257
+     */
258
+    public function getCipher() {
259
+        $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
260
+        if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
261
+            $this->logger->warning(
262
+                    sprintf(
263
+                            'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
264
+                            $cipher,
265
+                            self::DEFAULT_CIPHER
266
+                    ),
267
+                ['app' => 'encryption']);
268
+            $cipher = self::DEFAULT_CIPHER;
269
+        }
270
+
271
+        // Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
272
+        if(OPENSSL_VERSION_NUMBER < 0x1000101f) {
273
+            if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
274
+                $cipher = self::LEGACY_CIPHER;
275
+            }
276
+        }
277
+
278
+        return $cipher;
279
+    }
280
+
281
+    /**
282
+     * get key size depending on the cipher
283
+     *
284
+     * @param string $cipher
285
+     * @return int
286
+     * @throws \InvalidArgumentException
287
+     */
288
+    protected function getKeySize($cipher) {
289
+        if(isset($this->supportedCiphersAndKeySize[$cipher])) {
290
+            return $this->supportedCiphersAndKeySize[$cipher];
291
+        }
292
+
293
+        throw new \InvalidArgumentException(
294
+            sprintf(
295
+                    'Unsupported cipher (%s) defined.',
296
+                    $cipher
297
+            )
298
+        );
299
+    }
300
+
301
+    /**
302
+     * get legacy cipher
303
+     *
304
+     * @return string
305
+     */
306
+    public function getLegacyCipher() {
307
+        return self::LEGACY_CIPHER;
308
+    }
309
+
310
+    /**
311
+     * @param string $encryptedContent
312
+     * @param string $iv
313
+     * @return string
314
+     */
315
+    private function concatIV($encryptedContent, $iv) {
316
+        return $encryptedContent . '00iv00' . $iv;
317
+    }
318
+
319
+    /**
320
+     * @param string $encryptedContent
321
+     * @param string $signature
322
+     * @return string
323
+     */
324
+    private function concatSig($encryptedContent, $signature) {
325
+        return $encryptedContent . '00sig00' . $signature;
326
+    }
327
+
328
+    /**
329
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
330
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
331
+     * encrypted content and is not used in any crypto primitive.
332
+     *
333
+     * @param string $data
334
+     * @return string
335
+     */
336
+    private function addPadding($data) {
337
+        return $data . 'xxx';
338
+    }
339
+
340
+    /**
341
+     * generate password hash used to encrypt the users private key
342
+     *
343
+     * @param string $password
344
+     * @param string $cipher
345
+     * @param string $uid only used for user keys
346
+     * @return string
347
+     */
348
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
349
+        $instanceId = $this->config->getSystemValue('instanceid');
350
+        $instanceSecret = $this->config->getSystemValue('secret');
351
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
352
+        $keySize = $this->getKeySize($cipher);
353
+
354
+        $hash = hash_pbkdf2(
355
+            'sha256',
356
+            $password,
357
+            $salt,
358
+            100000,
359
+            $keySize,
360
+            true
361
+        );
362
+
363
+        return $hash;
364
+    }
365
+
366
+    /**
367
+     * encrypt private key
368
+     *
369
+     * @param string $privateKey
370
+     * @param string $password
371
+     * @param string $uid for regular users, empty for system keys
372
+     * @return false|string
373
+     */
374
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
375
+        $cipher = $this->getCipher();
376
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
377
+        $encryptedKey = $this->symmetricEncryptFileContent(
378
+            $privateKey,
379
+            $hash,
380
+            0,
381
+            0
382
+        );
383
+
384
+        return $encryptedKey;
385
+    }
386
+
387
+    /**
388
+     * @param string $privateKey
389
+     * @param string $password
390
+     * @param string $uid for regular users, empty for system keys
391
+     * @return false|string
392
+     */
393
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
394
+
395
+        $header = $this->parseHeader($privateKey);
396
+
397
+        if (isset($header['cipher'])) {
398
+            $cipher = $header['cipher'];
399
+        } else {
400
+            $cipher = self::LEGACY_CIPHER;
401
+        }
402
+
403
+        if (isset($header['keyFormat'])) {
404
+            $keyFormat = $header['keyFormat'];
405
+        } else {
406
+            $keyFormat = self::LEGACY_KEY_FORMAT;
407
+        }
408
+
409
+        if ($keyFormat === 'hash') {
410
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
411
+        }
412
+
413
+        // If we found a header we need to remove it from the key we want to decrypt
414
+        if (!empty($header)) {
415
+            $privateKey = substr($privateKey,
416
+                strpos($privateKey,
417
+                    self::HEADER_END) + strlen(self::HEADER_END));
418
+        }
419
+
420
+        $plainKey = $this->symmetricDecryptFileContent(
421
+            $privateKey,
422
+            $password,
423
+            $cipher,
424
+            0
425
+        );
426
+
427
+        if ($this->isValidPrivateKey($plainKey) === false) {
428
+            return false;
429
+        }
430
+
431
+        return $plainKey;
432
+    }
433
+
434
+    /**
435
+     * check if it is a valid private key
436
+     *
437
+     * @param string $plainKey
438
+     * @return bool
439
+     */
440
+    protected function isValidPrivateKey($plainKey) {
441
+        $res = openssl_get_privatekey($plainKey);
442
+        if (is_resource($res)) {
443
+            $sslInfo = openssl_pkey_get_details($res);
444
+            if (isset($sslInfo['key'])) {
445
+                return true;
446
+            }
447
+        }
448
+
449
+        return false;
450
+    }
451
+
452
+    /**
453
+     * @param string $keyFileContents
454
+     * @param string $passPhrase
455
+     * @param string $cipher
456
+     * @param int $version
457
+     * @param int $position
458
+     * @return string
459
+     * @throws DecryptionFailedException
460
+     */
461
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
462
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
463
+
464
+        if ($catFile['signature'] !== false) {
465
+            $this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
466
+        }
467
+
468
+        return $this->decrypt($catFile['encrypted'],
469
+            $catFile['iv'],
470
+            $passPhrase,
471
+            $cipher);
472
+    }
473
+
474
+    /**
475
+     * check for valid signature
476
+     *
477
+     * @param string $data
478
+     * @param string $passPhrase
479
+     * @param string $expectedSignature
480
+     * @throws GenericEncryptionException
481
+     */
482
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
483
+        $signature = $this->createSignature($data, $passPhrase);
484
+        if (!hash_equals($expectedSignature, $signature)) {
485
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
486
+        }
487
+    }
488
+
489
+    /**
490
+     * create signature
491
+     *
492
+     * @param string $data
493
+     * @param string $passPhrase
494
+     * @return string
495
+     */
496
+    private function createSignature($data, $passPhrase) {
497
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
498
+        $signature = hash_hmac('sha256', $data, $passPhrase);
499
+        return $signature;
500
+    }
501
+
502
+
503
+    /**
504
+     * remove padding
505
+     *
506
+     * @param string $padded
507
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
508
+     * @return string|false
509
+     */
510
+    private function removePadding($padded, $hasSignature = false) {
511
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
512
+            return substr($padded, 0, -2);
513
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
514
+            return substr($padded, 0, -3);
515
+        }
516
+        return false;
517
+    }
518
+
519
+    /**
520
+     * split meta data from encrypted file
521
+     * Note: for now, we assume that the meta data always start with the iv
522
+     *       followed by the signature, if available
523
+     *
524
+     * @param string $catFile
525
+     * @param string $cipher
526
+     * @return array
527
+     */
528
+    private function splitMetaData($catFile, $cipher) {
529
+        if ($this->hasSignature($catFile, $cipher)) {
530
+            $catFile = $this->removePadding($catFile, true);
531
+            $meta = substr($catFile, -93);
532
+            $iv = substr($meta, strlen('00iv00'), 16);
533
+            $sig = substr($meta, 22 + strlen('00sig00'));
534
+            $encrypted = substr($catFile, 0, -93);
535
+        } else {
536
+            $catFile = $this->removePadding($catFile);
537
+            $meta = substr($catFile, -22);
538
+            $iv = substr($meta, -16);
539
+            $sig = false;
540
+            $encrypted = substr($catFile, 0, -22);
541
+        }
542
+
543
+        return [
544
+            'encrypted' => $encrypted,
545
+            'iv' => $iv,
546
+            'signature' => $sig
547
+        ];
548
+    }
549
+
550
+    /**
551
+     * check if encrypted block is signed
552
+     *
553
+     * @param string $catFile
554
+     * @param string $cipher
555
+     * @return bool
556
+     * @throws GenericEncryptionException
557
+     */
558
+    private function hasSignature($catFile, $cipher) {
559
+        $meta = substr($catFile, -93);
560
+        $signaturePosition = strpos($meta, '00sig00');
561
+
562
+        // enforce signature for the new 'CTR' ciphers
563
+        if ($signaturePosition === false && stripos($cipher, 'ctr') !== false) {
564
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
565
+        }
566
+
567
+        return ($signaturePosition !== false);
568
+    }
569
+
570
+
571
+    /**
572
+     * @param string $encryptedContent
573
+     * @param string $iv
574
+     * @param string $passPhrase
575
+     * @param string $cipher
576
+     * @return string
577
+     * @throws DecryptionFailedException
578
+     */
579
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
580
+        $plainContent = openssl_decrypt($encryptedContent,
581
+            $cipher,
582
+            $passPhrase,
583
+            false,
584
+            $iv);
585
+
586
+        if ($plainContent) {
587
+            return $plainContent;
588
+        } else {
589
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
590
+        }
591
+    }
592
+
593
+    /**
594
+     * @param string $data
595
+     * @return array
596
+     */
597
+    protected function parseHeader($data) {
598
+        $result = [];
599
+
600
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
601
+            $endAt = strpos($data, self::HEADER_END);
602
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
603
+
604
+            // +1 not to start with an ':' which would result in empty element at the beginning
605
+            $exploded = explode(':',
606
+                substr($header, strlen(self::HEADER_START) + 1));
607
+
608
+            $element = array_shift($exploded);
609
+
610
+            while ($element !== self::HEADER_END) {
611
+                $result[$element] = array_shift($exploded);
612
+                $element = array_shift($exploded);
613
+            }
614
+        }
615
+
616
+        return $result;
617
+    }
618
+
619
+    /**
620
+     * generate initialization vector
621
+     *
622
+     * @return string
623
+     * @throws GenericEncryptionException
624
+     */
625
+    private function generateIv() {
626
+        return random_bytes(16);
627
+    }
628
+
629
+    /**
630
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
631
+     * as file key
632
+     *
633
+     * @return string
634
+     * @throws \Exception
635
+     */
636
+    public function generateFileKey() {
637
+        return random_bytes(32);
638
+    }
639
+
640
+    /**
641
+     * @param $encKeyFile
642
+     * @param $shareKey
643
+     * @param $privateKey
644
+     * @return string
645
+     * @throws MultiKeyDecryptException
646
+     */
647
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
648
+        if (!$encKeyFile) {
649
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
650
+        }
651
+
652
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
653
+            return $plainContent;
654
+        } else {
655
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
656
+        }
657
+    }
658
+
659
+    /**
660
+     * @param string $plainContent
661
+     * @param array $keyFiles
662
+     * @return array
663
+     * @throws MultiKeyEncryptException
664
+     */
665
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
666
+        // openssl_seal returns false without errors if plaincontent is empty
667
+        // so trigger our own error
668
+        if (empty($plainContent)) {
669
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
670
+        }
671
+
672
+        // Set empty vars to be set by openssl by reference
673
+        $sealed = '';
674
+        $shareKeys = [];
675
+        $mappedShareKeys = [];
676
+
677
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
678
+            $i = 0;
679
+
680
+            // Ensure each shareKey is labelled with its corresponding key id
681
+            foreach ($keyFiles as $userId => $publicKey) {
682
+                $mappedShareKeys[$userId] = $shareKeys[$i];
683
+                $i++;
684
+            }
685
+
686
+            return [
687
+                'keys' => $mappedShareKeys,
688
+                'data' => $sealed
689
+            ];
690
+        } else {
691
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
692
+        }
693
+    }
694 694
 }
695 695
 
Please login to merge, or discard this patch.
apps/workflowengine/lib/Controller/RequestTime.php 2 patches
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -26,27 +26,27 @@
 block discarded – undo
26 26
 
27 27
 class RequestTime extends Controller {
28 28
 
29
-	/**
30
-	 * @NoAdminRequired
31
-	 *
32
-	 * @param string $search
33
-	 * @return JSONResponse
34
-	 */
35
-	public function getTimezones($search = '') {
36
-		$timezones = \DateTimeZone::listIdentifiers();
29
+    /**
30
+     * @NoAdminRequired
31
+     *
32
+     * @param string $search
33
+     * @return JSONResponse
34
+     */
35
+    public function getTimezones($search = '') {
36
+        $timezones = \DateTimeZone::listIdentifiers();
37 37
 
38
-		if ($search !== '') {
39
-			$timezones = array_filter($timezones, function ($timezone) use ($search) {
40
-				return stripos($timezone, $search) !== false;
41
-			});
42
-		}
38
+        if ($search !== '') {
39
+            $timezones = array_filter($timezones, function ($timezone) use ($search) {
40
+                return stripos($timezone, $search) !== false;
41
+            });
42
+        }
43 43
 
44
-		$timezones = array_slice($timezones, 0, 10);
44
+        $timezones = array_slice($timezones, 0, 10);
45 45
 
46
-		$response = [];
47
-		foreach ($timezones as $timezone) {
48
-			$response[$timezone] = $timezone;
49
-		}
50
-		return new JSONResponse($response);
51
-	}
46
+        $response = [];
47
+        foreach ($timezones as $timezone) {
48
+            $response[$timezone] = $timezone;
49
+        }
50
+        return new JSONResponse($response);
51
+    }
52 52
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -36,7 +36,7 @@
 block discarded – undo
36 36
 		$timezones = \DateTimeZone::listIdentifiers();
37 37
 
38 38
 		if ($search !== '') {
39
-			$timezones = array_filter($timezones, function ($timezone) use ($search) {
39
+			$timezones = array_filter($timezones, function($timezone) use ($search) {
40 40
 				return stripos($timezone, $search) !== false;
41 41
 			});
42 42
 		}
Please login to merge, or discard this patch.