Passed
Push — master ( 4361d6...b604d5 )
by Roeland
12:05 queued 10s
created
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +661 added lines, -661 removed lines patch added patch discarded remove patch
@@ -56,665 +56,665 @@
 block discarded – undo
56 56
  * @package OCA\Encryption\Crypto
57 57
  */
58 58
 class Crypt {
59
-	public const DEFAULT_CIPHER = 'AES-256-CTR';
60
-	// default cipher from old Nextcloud versions
61
-	public const LEGACY_CIPHER = 'AES-128-CFB';
62
-
63
-	// default key format, old Nextcloud version encrypted the private key directly
64
-	// with the user password
65
-	public const LEGACY_KEY_FORMAT = 'password';
66
-
67
-	public const HEADER_START = 'HBEGIN';
68
-	public const HEADER_END = 'HEND';
69
-
70
-	/** @var ILogger */
71
-	private $logger;
72
-
73
-	/** @var string */
74
-	private $user;
75
-
76
-	/** @var IConfig */
77
-	private $config;
78
-
79
-	/** @var array */
80
-	private $supportedKeyFormats;
81
-
82
-	/** @var IL10N */
83
-	private $l;
84
-
85
-	/** @var array */
86
-	private $supportedCiphersAndKeySize = [
87
-		'AES-256-CTR' => 32,
88
-		'AES-128-CTR' => 16,
89
-		'AES-256-CFB' => 32,
90
-		'AES-128-CFB' => 16,
91
-	];
92
-
93
-	/** @var bool */
94
-	private $supportLegacy;
95
-
96
-	/**
97
-	 * @param ILogger $logger
98
-	 * @param IUserSession $userSession
99
-	 * @param IConfig $config
100
-	 * @param IL10N $l
101
-	 */
102
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
103
-		$this->logger = $logger;
104
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
105
-		$this->config = $config;
106
-		$this->l = $l;
107
-		$this->supportedKeyFormats = ['hash', 'password'];
108
-
109
-		$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
110
-	}
111
-
112
-	/**
113
-	 * create new private/public key-pair for user
114
-	 *
115
-	 * @return array|bool
116
-	 */
117
-	public function createKeyPair() {
118
-		$log = $this->logger;
119
-		$res = $this->getOpenSSLPKey();
120
-
121
-		if (!$res) {
122
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
123
-				['app' => 'encryption']);
124
-
125
-			if (openssl_error_string()) {
126
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
127
-					['app' => 'encryption']);
128
-			}
129
-		} elseif (openssl_pkey_export($res,
130
-			$privateKey,
131
-			null,
132
-			$this->getOpenSSLConfig())) {
133
-			$keyDetails = openssl_pkey_get_details($res);
134
-			$publicKey = $keyDetails['key'];
135
-
136
-			return [
137
-				'publicKey' => $publicKey,
138
-				'privateKey' => $privateKey
139
-			];
140
-		}
141
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
142
-			['app' => 'encryption']);
143
-		if (openssl_error_string()) {
144
-			$log->error('Encryption Library:' . openssl_error_string(),
145
-				['app' => 'encryption']);
146
-		}
147
-
148
-		return false;
149
-	}
150
-
151
-	/**
152
-	 * Generates a new private key
153
-	 *
154
-	 * @return resource
155
-	 */
156
-	public function getOpenSSLPKey() {
157
-		$config = $this->getOpenSSLConfig();
158
-		return openssl_pkey_new($config);
159
-	}
160
-
161
-	/**
162
-	 * get openSSL Config
163
-	 *
164
-	 * @return array
165
-	 */
166
-	private function getOpenSSLConfig() {
167
-		$config = ['private_key_bits' => 4096];
168
-		$config = array_merge(
169
-			$config,
170
-			$this->config->getSystemValue('openssl', [])
171
-		);
172
-		return $config;
173
-	}
174
-
175
-	/**
176
-	 * @param string $plainContent
177
-	 * @param string $passPhrase
178
-	 * @param int $version
179
-	 * @param int $position
180
-	 * @return false|string
181
-	 * @throws EncryptionFailedException
182
-	 */
183
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
184
-		if (!$plainContent) {
185
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
186
-				['app' => 'encryption']);
187
-			return false;
188
-		}
189
-
190
-		$iv = $this->generateIv();
191
-
192
-		$encryptedContent = $this->encrypt($plainContent,
193
-			$iv,
194
-			$passPhrase,
195
-			$this->getCipher());
196
-
197
-		// Create a signature based on the key as well as the current version
198
-		$sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
199
-
200
-		// combine content to encrypt the IV identifier and actual IV
201
-		$catFile = $this->concatIV($encryptedContent, $iv);
202
-		$catFile = $this->concatSig($catFile, $sig);
203
-		return $this->addPadding($catFile);
204
-	}
205
-
206
-	/**
207
-	 * generate header for encrypted file
208
-	 *
209
-	 * @param string $keyFormat (can be 'hash' or 'password')
210
-	 * @return string
211
-	 * @throws \InvalidArgumentException
212
-	 */
213
-	public function generateHeader($keyFormat = 'hash') {
214
-		if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
215
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
216
-		}
217
-
218
-		$cipher = $this->getCipher();
219
-
220
-		$header = self::HEADER_START
221
-			. ':cipher:' . $cipher
222
-			. ':keyFormat:' . $keyFormat
223
-			. ':' . self::HEADER_END;
224
-
225
-		return $header;
226
-	}
227
-
228
-	/**
229
-	 * @param string $plainContent
230
-	 * @param string $iv
231
-	 * @param string $passPhrase
232
-	 * @param string $cipher
233
-	 * @return string
234
-	 * @throws EncryptionFailedException
235
-	 */
236
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
237
-		$encryptedContent = openssl_encrypt($plainContent,
238
-			$cipher,
239
-			$passPhrase,
240
-			false,
241
-			$iv);
242
-
243
-		if (!$encryptedContent) {
244
-			$error = 'Encryption (symmetric) of content failed';
245
-			$this->logger->error($error . openssl_error_string(),
246
-				['app' => 'encryption']);
247
-			throw new EncryptionFailedException($error);
248
-		}
249
-
250
-		return $encryptedContent;
251
-	}
252
-
253
-	/**
254
-	 * return Cipher either from config.php or the default cipher defined in
255
-	 * this class
256
-	 *
257
-	 * @return string
258
-	 */
259
-	public function getCipher() {
260
-		$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
261
-		if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
262
-			$this->logger->warning(
263
-					sprintf(
264
-							'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
265
-							$cipher,
266
-							self::DEFAULT_CIPHER
267
-					),
268
-				['app' => 'encryption']);
269
-			$cipher = self::DEFAULT_CIPHER;
270
-		}
271
-
272
-		// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
273
-		if (OPENSSL_VERSION_NUMBER < 0x1000101f) {
274
-			if ($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
275
-				$cipher = self::LEGACY_CIPHER;
276
-			}
277
-		}
278
-
279
-		return $cipher;
280
-	}
281
-
282
-	/**
283
-	 * get key size depending on the cipher
284
-	 *
285
-	 * @param string $cipher
286
-	 * @return int
287
-	 * @throws \InvalidArgumentException
288
-	 */
289
-	protected function getKeySize($cipher) {
290
-		if (isset($this->supportedCiphersAndKeySize[$cipher])) {
291
-			return $this->supportedCiphersAndKeySize[$cipher];
292
-		}
293
-
294
-		throw new \InvalidArgumentException(
295
-			sprintf(
296
-					'Unsupported cipher (%s) defined.',
297
-					$cipher
298
-			)
299
-		);
300
-	}
301
-
302
-	/**
303
-	 * get legacy cipher
304
-	 *
305
-	 * @return string
306
-	 */
307
-	public function getLegacyCipher() {
308
-		if (!$this->supportLegacy) {
309
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
310
-		}
311
-
312
-		return self::LEGACY_CIPHER;
313
-	}
314
-
315
-	/**
316
-	 * @param string $encryptedContent
317
-	 * @param string $iv
318
-	 * @return string
319
-	 */
320
-	private function concatIV($encryptedContent, $iv) {
321
-		return $encryptedContent . '00iv00' . $iv;
322
-	}
323
-
324
-	/**
325
-	 * @param string $encryptedContent
326
-	 * @param string $signature
327
-	 * @return string
328
-	 */
329
-	private function concatSig($encryptedContent, $signature) {
330
-		return $encryptedContent . '00sig00' . $signature;
331
-	}
332
-
333
-	/**
334
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
335
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
336
-	 * encrypted content and is not used in any crypto primitive.
337
-	 *
338
-	 * @param string $data
339
-	 * @return string
340
-	 */
341
-	private function addPadding($data) {
342
-		return $data . 'xxx';
343
-	}
344
-
345
-	/**
346
-	 * generate password hash used to encrypt the users private key
347
-	 *
348
-	 * @param string $password
349
-	 * @param string $cipher
350
-	 * @param string $uid only used for user keys
351
-	 * @return string
352
-	 */
353
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
354
-		$instanceId = $this->config->getSystemValue('instanceid');
355
-		$instanceSecret = $this->config->getSystemValue('secret');
356
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
357
-		$keySize = $this->getKeySize($cipher);
358
-
359
-		$hash = hash_pbkdf2(
360
-			'sha256',
361
-			$password,
362
-			$salt,
363
-			100000,
364
-			$keySize,
365
-			true
366
-		);
367
-
368
-		return $hash;
369
-	}
370
-
371
-	/**
372
-	 * encrypt private key
373
-	 *
374
-	 * @param string $privateKey
375
-	 * @param string $password
376
-	 * @param string $uid for regular users, empty for system keys
377
-	 * @return false|string
378
-	 */
379
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
380
-		$cipher = $this->getCipher();
381
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
382
-		$encryptedKey = $this->symmetricEncryptFileContent(
383
-			$privateKey,
384
-			$hash,
385
-			0,
386
-			0
387
-		);
388
-
389
-		return $encryptedKey;
390
-	}
391
-
392
-	/**
393
-	 * @param string $privateKey
394
-	 * @param string $password
395
-	 * @param string $uid for regular users, empty for system keys
396
-	 * @return false|string
397
-	 */
398
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
399
-		$header = $this->parseHeader($privateKey);
400
-
401
-		if (isset($header['cipher'])) {
402
-			$cipher = $header['cipher'];
403
-		} else {
404
-			$cipher = $this->getLegacyCipher();
405
-		}
406
-
407
-		if (isset($header['keyFormat'])) {
408
-			$keyFormat = $header['keyFormat'];
409
-		} else {
410
-			$keyFormat = self::LEGACY_KEY_FORMAT;
411
-		}
412
-
413
-		if ($keyFormat === 'hash') {
414
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
415
-		}
416
-
417
-		// If we found a header we need to remove it from the key we want to decrypt
418
-		if (!empty($header)) {
419
-			$privateKey = substr($privateKey,
420
-				strpos($privateKey,
421
-					self::HEADER_END) + strlen(self::HEADER_END));
422
-		}
423
-
424
-		$plainKey = $this->symmetricDecryptFileContent(
425
-			$privateKey,
426
-			$password,
427
-			$cipher,
428
-			0
429
-		);
430
-
431
-		if ($this->isValidPrivateKey($plainKey) === false) {
432
-			return false;
433
-		}
434
-
435
-		return $plainKey;
436
-	}
437
-
438
-	/**
439
-	 * check if it is a valid private key
440
-	 *
441
-	 * @param string $plainKey
442
-	 * @return bool
443
-	 */
444
-	protected function isValidPrivateKey($plainKey) {
445
-		$res = openssl_get_privatekey($plainKey);
446
-		if (is_resource($res)) {
447
-			$sslInfo = openssl_pkey_get_details($res);
448
-			if (isset($sslInfo['key'])) {
449
-				return true;
450
-			}
451
-		}
452
-
453
-		return false;
454
-	}
455
-
456
-	/**
457
-	 * @param string $keyFileContents
458
-	 * @param string $passPhrase
459
-	 * @param string $cipher
460
-	 * @param int $version
461
-	 * @param int $position
462
-	 * @return string
463
-	 * @throws DecryptionFailedException
464
-	 */
465
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
466
-		if ($keyFileContents == '') {
467
-			return '';
468
-		}
469
-
470
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
471
-
472
-		if ($catFile['signature'] !== false) {
473
-			try {
474
-				// First try the new format
475
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
476
-			} catch (GenericEncryptionException $e) {
477
-				// For compatibility with old files check the version without _
478
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
479
-			}
480
-		}
481
-
482
-		return $this->decrypt($catFile['encrypted'],
483
-			$catFile['iv'],
484
-			$passPhrase,
485
-			$cipher);
486
-	}
487
-
488
-	/**
489
-	 * check for valid signature
490
-	 *
491
-	 * @param string $data
492
-	 * @param string $passPhrase
493
-	 * @param string $expectedSignature
494
-	 * @throws GenericEncryptionException
495
-	 */
496
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
497
-		$enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
498
-
499
-		$signature = $this->createSignature($data, $passPhrase);
500
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
501
-
502
-		if (!$isCorrectHash && $enforceSignature) {
503
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
504
-		} elseif (!$isCorrectHash && !$enforceSignature) {
505
-			$this->logger->info("Signature check skipped", ['app' => 'encryption']);
506
-		}
507
-	}
508
-
509
-	/**
510
-	 * create signature
511
-	 *
512
-	 * @param string $data
513
-	 * @param string $passPhrase
514
-	 * @return string
515
-	 */
516
-	private function createSignature($data, $passPhrase) {
517
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
518
-		return hash_hmac('sha256', $data, $passPhrase);
519
-	}
520
-
521
-
522
-	/**
523
-	 * remove padding
524
-	 *
525
-	 * @param string $padded
526
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
527
-	 * @return string|false
528
-	 */
529
-	private function removePadding($padded, $hasSignature = false) {
530
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
531
-			return substr($padded, 0, -2);
532
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
533
-			return substr($padded, 0, -3);
534
-		}
535
-		return false;
536
-	}
537
-
538
-	/**
539
-	 * split meta data from encrypted file
540
-	 * Note: for now, we assume that the meta data always start with the iv
541
-	 *       followed by the signature, if available
542
-	 *
543
-	 * @param string $catFile
544
-	 * @param string $cipher
545
-	 * @return array
546
-	 */
547
-	private function splitMetaData($catFile, $cipher) {
548
-		if ($this->hasSignature($catFile, $cipher)) {
549
-			$catFile = $this->removePadding($catFile, true);
550
-			$meta = substr($catFile, -93);
551
-			$iv = substr($meta, strlen('00iv00'), 16);
552
-			$sig = substr($meta, 22 + strlen('00sig00'));
553
-			$encrypted = substr($catFile, 0, -93);
554
-		} else {
555
-			$catFile = $this->removePadding($catFile);
556
-			$meta = substr($catFile, -22);
557
-			$iv = substr($meta, -16);
558
-			$sig = false;
559
-			$encrypted = substr($catFile, 0, -22);
560
-		}
561
-
562
-		return [
563
-			'encrypted' => $encrypted,
564
-			'iv' => $iv,
565
-			'signature' => $sig
566
-		];
567
-	}
568
-
569
-	/**
570
-	 * check if encrypted block is signed
571
-	 *
572
-	 * @param string $catFile
573
-	 * @param string $cipher
574
-	 * @return bool
575
-	 * @throws GenericEncryptionException
576
-	 */
577
-	private function hasSignature($catFile, $cipher) {
578
-		$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
579
-
580
-		$meta = substr($catFile, -93);
581
-		$signaturePosition = strpos($meta, '00sig00');
582
-
583
-		// If we no longer support the legacy format then everything needs a signature
584
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
585
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
586
-		}
587
-
588
-		// enforce signature for the new 'CTR' ciphers
589
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
590
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
591
-		}
592
-
593
-		return ($signaturePosition !== false);
594
-	}
595
-
596
-
597
-	/**
598
-	 * @param string $encryptedContent
599
-	 * @param string $iv
600
-	 * @param string $passPhrase
601
-	 * @param string $cipher
602
-	 * @return string
603
-	 * @throws DecryptionFailedException
604
-	 */
605
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
606
-		$plainContent = openssl_decrypt($encryptedContent,
607
-			$cipher,
608
-			$passPhrase,
609
-			false,
610
-			$iv);
611
-
612
-		if ($plainContent) {
613
-			return $plainContent;
614
-		} else {
615
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
616
-		}
617
-	}
618
-
619
-	/**
620
-	 * @param string $data
621
-	 * @return array
622
-	 */
623
-	protected function parseHeader($data) {
624
-		$result = [];
625
-
626
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
627
-			$endAt = strpos($data, self::HEADER_END);
628
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
629
-
630
-			// +1 not to start with an ':' which would result in empty element at the beginning
631
-			$exploded = explode(':',
632
-				substr($header, strlen(self::HEADER_START) + 1));
633
-
634
-			$element = array_shift($exploded);
635
-
636
-			while ($element !== self::HEADER_END) {
637
-				$result[$element] = array_shift($exploded);
638
-				$element = array_shift($exploded);
639
-			}
640
-		}
641
-
642
-		return $result;
643
-	}
644
-
645
-	/**
646
-	 * generate initialization vector
647
-	 *
648
-	 * @return string
649
-	 * @throws GenericEncryptionException
650
-	 */
651
-	private function generateIv() {
652
-		return random_bytes(16);
653
-	}
654
-
655
-	/**
656
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
657
-	 * as file key
658
-	 *
659
-	 * @return string
660
-	 * @throws \Exception
661
-	 */
662
-	public function generateFileKey() {
663
-		return random_bytes(32);
664
-	}
665
-
666
-	/**
667
-	 * @param $encKeyFile
668
-	 * @param $shareKey
669
-	 * @param $privateKey
670
-	 * @return string
671
-	 * @throws MultiKeyDecryptException
672
-	 */
673
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
674
-		if (!$encKeyFile) {
675
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
676
-		}
677
-
678
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
679
-			return $plainContent;
680
-		} else {
681
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
682
-		}
683
-	}
684
-
685
-	/**
686
-	 * @param string $plainContent
687
-	 * @param array $keyFiles
688
-	 * @return array
689
-	 * @throws MultiKeyEncryptException
690
-	 */
691
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
692
-		// openssl_seal returns false without errors if plaincontent is empty
693
-		// so trigger our own error
694
-		if (empty($plainContent)) {
695
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
696
-		}
697
-
698
-		// Set empty vars to be set by openssl by reference
699
-		$sealed = '';
700
-		$shareKeys = [];
701
-		$mappedShareKeys = [];
702
-
703
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
704
-			$i = 0;
705
-
706
-			// Ensure each shareKey is labelled with its corresponding key id
707
-			foreach ($keyFiles as $userId => $publicKey) {
708
-				$mappedShareKeys[$userId] = $shareKeys[$i];
709
-				$i++;
710
-			}
711
-
712
-			return [
713
-				'keys' => $mappedShareKeys,
714
-				'data' => $sealed
715
-			];
716
-		} else {
717
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
718
-		}
719
-	}
59
+    public const DEFAULT_CIPHER = 'AES-256-CTR';
60
+    // default cipher from old Nextcloud versions
61
+    public const LEGACY_CIPHER = 'AES-128-CFB';
62
+
63
+    // default key format, old Nextcloud version encrypted the private key directly
64
+    // with the user password
65
+    public const LEGACY_KEY_FORMAT = 'password';
66
+
67
+    public const HEADER_START = 'HBEGIN';
68
+    public const HEADER_END = 'HEND';
69
+
70
+    /** @var ILogger */
71
+    private $logger;
72
+
73
+    /** @var string */
74
+    private $user;
75
+
76
+    /** @var IConfig */
77
+    private $config;
78
+
79
+    /** @var array */
80
+    private $supportedKeyFormats;
81
+
82
+    /** @var IL10N */
83
+    private $l;
84
+
85
+    /** @var array */
86
+    private $supportedCiphersAndKeySize = [
87
+        'AES-256-CTR' => 32,
88
+        'AES-128-CTR' => 16,
89
+        'AES-256-CFB' => 32,
90
+        'AES-128-CFB' => 16,
91
+    ];
92
+
93
+    /** @var bool */
94
+    private $supportLegacy;
95
+
96
+    /**
97
+     * @param ILogger $logger
98
+     * @param IUserSession $userSession
99
+     * @param IConfig $config
100
+     * @param IL10N $l
101
+     */
102
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
103
+        $this->logger = $logger;
104
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
105
+        $this->config = $config;
106
+        $this->l = $l;
107
+        $this->supportedKeyFormats = ['hash', 'password'];
108
+
109
+        $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
110
+    }
111
+
112
+    /**
113
+     * create new private/public key-pair for user
114
+     *
115
+     * @return array|bool
116
+     */
117
+    public function createKeyPair() {
118
+        $log = $this->logger;
119
+        $res = $this->getOpenSSLPKey();
120
+
121
+        if (!$res) {
122
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
123
+                ['app' => 'encryption']);
124
+
125
+            if (openssl_error_string()) {
126
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
127
+                    ['app' => 'encryption']);
128
+            }
129
+        } elseif (openssl_pkey_export($res,
130
+            $privateKey,
131
+            null,
132
+            $this->getOpenSSLConfig())) {
133
+            $keyDetails = openssl_pkey_get_details($res);
134
+            $publicKey = $keyDetails['key'];
135
+
136
+            return [
137
+                'publicKey' => $publicKey,
138
+                'privateKey' => $privateKey
139
+            ];
140
+        }
141
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
142
+            ['app' => 'encryption']);
143
+        if (openssl_error_string()) {
144
+            $log->error('Encryption Library:' . openssl_error_string(),
145
+                ['app' => 'encryption']);
146
+        }
147
+
148
+        return false;
149
+    }
150
+
151
+    /**
152
+     * Generates a new private key
153
+     *
154
+     * @return resource
155
+     */
156
+    public function getOpenSSLPKey() {
157
+        $config = $this->getOpenSSLConfig();
158
+        return openssl_pkey_new($config);
159
+    }
160
+
161
+    /**
162
+     * get openSSL Config
163
+     *
164
+     * @return array
165
+     */
166
+    private function getOpenSSLConfig() {
167
+        $config = ['private_key_bits' => 4096];
168
+        $config = array_merge(
169
+            $config,
170
+            $this->config->getSystemValue('openssl', [])
171
+        );
172
+        return $config;
173
+    }
174
+
175
+    /**
176
+     * @param string $plainContent
177
+     * @param string $passPhrase
178
+     * @param int $version
179
+     * @param int $position
180
+     * @return false|string
181
+     * @throws EncryptionFailedException
182
+     */
183
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
184
+        if (!$plainContent) {
185
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
186
+                ['app' => 'encryption']);
187
+            return false;
188
+        }
189
+
190
+        $iv = $this->generateIv();
191
+
192
+        $encryptedContent = $this->encrypt($plainContent,
193
+            $iv,
194
+            $passPhrase,
195
+            $this->getCipher());
196
+
197
+        // Create a signature based on the key as well as the current version
198
+        $sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
199
+
200
+        // combine content to encrypt the IV identifier and actual IV
201
+        $catFile = $this->concatIV($encryptedContent, $iv);
202
+        $catFile = $this->concatSig($catFile, $sig);
203
+        return $this->addPadding($catFile);
204
+    }
205
+
206
+    /**
207
+     * generate header for encrypted file
208
+     *
209
+     * @param string $keyFormat (can be 'hash' or 'password')
210
+     * @return string
211
+     * @throws \InvalidArgumentException
212
+     */
213
+    public function generateHeader($keyFormat = 'hash') {
214
+        if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
215
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
216
+        }
217
+
218
+        $cipher = $this->getCipher();
219
+
220
+        $header = self::HEADER_START
221
+            . ':cipher:' . $cipher
222
+            . ':keyFormat:' . $keyFormat
223
+            . ':' . self::HEADER_END;
224
+
225
+        return $header;
226
+    }
227
+
228
+    /**
229
+     * @param string $plainContent
230
+     * @param string $iv
231
+     * @param string $passPhrase
232
+     * @param string $cipher
233
+     * @return string
234
+     * @throws EncryptionFailedException
235
+     */
236
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
237
+        $encryptedContent = openssl_encrypt($plainContent,
238
+            $cipher,
239
+            $passPhrase,
240
+            false,
241
+            $iv);
242
+
243
+        if (!$encryptedContent) {
244
+            $error = 'Encryption (symmetric) of content failed';
245
+            $this->logger->error($error . openssl_error_string(),
246
+                ['app' => 'encryption']);
247
+            throw new EncryptionFailedException($error);
248
+        }
249
+
250
+        return $encryptedContent;
251
+    }
252
+
253
+    /**
254
+     * return Cipher either from config.php or the default cipher defined in
255
+     * this class
256
+     *
257
+     * @return string
258
+     */
259
+    public function getCipher() {
260
+        $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
261
+        if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
262
+            $this->logger->warning(
263
+                    sprintf(
264
+                            'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
265
+                            $cipher,
266
+                            self::DEFAULT_CIPHER
267
+                    ),
268
+                ['app' => 'encryption']);
269
+            $cipher = self::DEFAULT_CIPHER;
270
+        }
271
+
272
+        // Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
273
+        if (OPENSSL_VERSION_NUMBER < 0x1000101f) {
274
+            if ($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
275
+                $cipher = self::LEGACY_CIPHER;
276
+            }
277
+        }
278
+
279
+        return $cipher;
280
+    }
281
+
282
+    /**
283
+     * get key size depending on the cipher
284
+     *
285
+     * @param string $cipher
286
+     * @return int
287
+     * @throws \InvalidArgumentException
288
+     */
289
+    protected function getKeySize($cipher) {
290
+        if (isset($this->supportedCiphersAndKeySize[$cipher])) {
291
+            return $this->supportedCiphersAndKeySize[$cipher];
292
+        }
293
+
294
+        throw new \InvalidArgumentException(
295
+            sprintf(
296
+                    'Unsupported cipher (%s) defined.',
297
+                    $cipher
298
+            )
299
+        );
300
+    }
301
+
302
+    /**
303
+     * get legacy cipher
304
+     *
305
+     * @return string
306
+     */
307
+    public function getLegacyCipher() {
308
+        if (!$this->supportLegacy) {
309
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
310
+        }
311
+
312
+        return self::LEGACY_CIPHER;
313
+    }
314
+
315
+    /**
316
+     * @param string $encryptedContent
317
+     * @param string $iv
318
+     * @return string
319
+     */
320
+    private function concatIV($encryptedContent, $iv) {
321
+        return $encryptedContent . '00iv00' . $iv;
322
+    }
323
+
324
+    /**
325
+     * @param string $encryptedContent
326
+     * @param string $signature
327
+     * @return string
328
+     */
329
+    private function concatSig($encryptedContent, $signature) {
330
+        return $encryptedContent . '00sig00' . $signature;
331
+    }
332
+
333
+    /**
334
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
335
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
336
+     * encrypted content and is not used in any crypto primitive.
337
+     *
338
+     * @param string $data
339
+     * @return string
340
+     */
341
+    private function addPadding($data) {
342
+        return $data . 'xxx';
343
+    }
344
+
345
+    /**
346
+     * generate password hash used to encrypt the users private key
347
+     *
348
+     * @param string $password
349
+     * @param string $cipher
350
+     * @param string $uid only used for user keys
351
+     * @return string
352
+     */
353
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
354
+        $instanceId = $this->config->getSystemValue('instanceid');
355
+        $instanceSecret = $this->config->getSystemValue('secret');
356
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
357
+        $keySize = $this->getKeySize($cipher);
358
+
359
+        $hash = hash_pbkdf2(
360
+            'sha256',
361
+            $password,
362
+            $salt,
363
+            100000,
364
+            $keySize,
365
+            true
366
+        );
367
+
368
+        return $hash;
369
+    }
370
+
371
+    /**
372
+     * encrypt private key
373
+     *
374
+     * @param string $privateKey
375
+     * @param string $password
376
+     * @param string $uid for regular users, empty for system keys
377
+     * @return false|string
378
+     */
379
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
380
+        $cipher = $this->getCipher();
381
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
382
+        $encryptedKey = $this->symmetricEncryptFileContent(
383
+            $privateKey,
384
+            $hash,
385
+            0,
386
+            0
387
+        );
388
+
389
+        return $encryptedKey;
390
+    }
391
+
392
+    /**
393
+     * @param string $privateKey
394
+     * @param string $password
395
+     * @param string $uid for regular users, empty for system keys
396
+     * @return false|string
397
+     */
398
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
399
+        $header = $this->parseHeader($privateKey);
400
+
401
+        if (isset($header['cipher'])) {
402
+            $cipher = $header['cipher'];
403
+        } else {
404
+            $cipher = $this->getLegacyCipher();
405
+        }
406
+
407
+        if (isset($header['keyFormat'])) {
408
+            $keyFormat = $header['keyFormat'];
409
+        } else {
410
+            $keyFormat = self::LEGACY_KEY_FORMAT;
411
+        }
412
+
413
+        if ($keyFormat === 'hash') {
414
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
415
+        }
416
+
417
+        // If we found a header we need to remove it from the key we want to decrypt
418
+        if (!empty($header)) {
419
+            $privateKey = substr($privateKey,
420
+                strpos($privateKey,
421
+                    self::HEADER_END) + strlen(self::HEADER_END));
422
+        }
423
+
424
+        $plainKey = $this->symmetricDecryptFileContent(
425
+            $privateKey,
426
+            $password,
427
+            $cipher,
428
+            0
429
+        );
430
+
431
+        if ($this->isValidPrivateKey($plainKey) === false) {
432
+            return false;
433
+        }
434
+
435
+        return $plainKey;
436
+    }
437
+
438
+    /**
439
+     * check if it is a valid private key
440
+     *
441
+     * @param string $plainKey
442
+     * @return bool
443
+     */
444
+    protected function isValidPrivateKey($plainKey) {
445
+        $res = openssl_get_privatekey($plainKey);
446
+        if (is_resource($res)) {
447
+            $sslInfo = openssl_pkey_get_details($res);
448
+            if (isset($sslInfo['key'])) {
449
+                return true;
450
+            }
451
+        }
452
+
453
+        return false;
454
+    }
455
+
456
+    /**
457
+     * @param string $keyFileContents
458
+     * @param string $passPhrase
459
+     * @param string $cipher
460
+     * @param int $version
461
+     * @param int $position
462
+     * @return string
463
+     * @throws DecryptionFailedException
464
+     */
465
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
466
+        if ($keyFileContents == '') {
467
+            return '';
468
+        }
469
+
470
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
471
+
472
+        if ($catFile['signature'] !== false) {
473
+            try {
474
+                // First try the new format
475
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
476
+            } catch (GenericEncryptionException $e) {
477
+                // For compatibility with old files check the version without _
478
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
479
+            }
480
+        }
481
+
482
+        return $this->decrypt($catFile['encrypted'],
483
+            $catFile['iv'],
484
+            $passPhrase,
485
+            $cipher);
486
+    }
487
+
488
+    /**
489
+     * check for valid signature
490
+     *
491
+     * @param string $data
492
+     * @param string $passPhrase
493
+     * @param string $expectedSignature
494
+     * @throws GenericEncryptionException
495
+     */
496
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
497
+        $enforceSignature = !$this->config->getSystemValue('encryption_skip_signature_check', false);
498
+
499
+        $signature = $this->createSignature($data, $passPhrase);
500
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
501
+
502
+        if (!$isCorrectHash && $enforceSignature) {
503
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
504
+        } elseif (!$isCorrectHash && !$enforceSignature) {
505
+            $this->logger->info("Signature check skipped", ['app' => 'encryption']);
506
+        }
507
+    }
508
+
509
+    /**
510
+     * create signature
511
+     *
512
+     * @param string $data
513
+     * @param string $passPhrase
514
+     * @return string
515
+     */
516
+    private function createSignature($data, $passPhrase) {
517
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
518
+        return hash_hmac('sha256', $data, $passPhrase);
519
+    }
520
+
521
+
522
+    /**
523
+     * remove padding
524
+     *
525
+     * @param string $padded
526
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
527
+     * @return string|false
528
+     */
529
+    private function removePadding($padded, $hasSignature = false) {
530
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
531
+            return substr($padded, 0, -2);
532
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
533
+            return substr($padded, 0, -3);
534
+        }
535
+        return false;
536
+    }
537
+
538
+    /**
539
+     * split meta data from encrypted file
540
+     * Note: for now, we assume that the meta data always start with the iv
541
+     *       followed by the signature, if available
542
+     *
543
+     * @param string $catFile
544
+     * @param string $cipher
545
+     * @return array
546
+     */
547
+    private function splitMetaData($catFile, $cipher) {
548
+        if ($this->hasSignature($catFile, $cipher)) {
549
+            $catFile = $this->removePadding($catFile, true);
550
+            $meta = substr($catFile, -93);
551
+            $iv = substr($meta, strlen('00iv00'), 16);
552
+            $sig = substr($meta, 22 + strlen('00sig00'));
553
+            $encrypted = substr($catFile, 0, -93);
554
+        } else {
555
+            $catFile = $this->removePadding($catFile);
556
+            $meta = substr($catFile, -22);
557
+            $iv = substr($meta, -16);
558
+            $sig = false;
559
+            $encrypted = substr($catFile, 0, -22);
560
+        }
561
+
562
+        return [
563
+            'encrypted' => $encrypted,
564
+            'iv' => $iv,
565
+            'signature' => $sig
566
+        ];
567
+    }
568
+
569
+    /**
570
+     * check if encrypted block is signed
571
+     *
572
+     * @param string $catFile
573
+     * @param string $cipher
574
+     * @return bool
575
+     * @throws GenericEncryptionException
576
+     */
577
+    private function hasSignature($catFile, $cipher) {
578
+        $skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
579
+
580
+        $meta = substr($catFile, -93);
581
+        $signaturePosition = strpos($meta, '00sig00');
582
+
583
+        // If we no longer support the legacy format then everything needs a signature
584
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
585
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
586
+        }
587
+
588
+        // enforce signature for the new 'CTR' ciphers
589
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
590
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
591
+        }
592
+
593
+        return ($signaturePosition !== false);
594
+    }
595
+
596
+
597
+    /**
598
+     * @param string $encryptedContent
599
+     * @param string $iv
600
+     * @param string $passPhrase
601
+     * @param string $cipher
602
+     * @return string
603
+     * @throws DecryptionFailedException
604
+     */
605
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
606
+        $plainContent = openssl_decrypt($encryptedContent,
607
+            $cipher,
608
+            $passPhrase,
609
+            false,
610
+            $iv);
611
+
612
+        if ($plainContent) {
613
+            return $plainContent;
614
+        } else {
615
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
616
+        }
617
+    }
618
+
619
+    /**
620
+     * @param string $data
621
+     * @return array
622
+     */
623
+    protected function parseHeader($data) {
624
+        $result = [];
625
+
626
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
627
+            $endAt = strpos($data, self::HEADER_END);
628
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
629
+
630
+            // +1 not to start with an ':' which would result in empty element at the beginning
631
+            $exploded = explode(':',
632
+                substr($header, strlen(self::HEADER_START) + 1));
633
+
634
+            $element = array_shift($exploded);
635
+
636
+            while ($element !== self::HEADER_END) {
637
+                $result[$element] = array_shift($exploded);
638
+                $element = array_shift($exploded);
639
+            }
640
+        }
641
+
642
+        return $result;
643
+    }
644
+
645
+    /**
646
+     * generate initialization vector
647
+     *
648
+     * @return string
649
+     * @throws GenericEncryptionException
650
+     */
651
+    private function generateIv() {
652
+        return random_bytes(16);
653
+    }
654
+
655
+    /**
656
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
657
+     * as file key
658
+     *
659
+     * @return string
660
+     * @throws \Exception
661
+     */
662
+    public function generateFileKey() {
663
+        return random_bytes(32);
664
+    }
665
+
666
+    /**
667
+     * @param $encKeyFile
668
+     * @param $shareKey
669
+     * @param $privateKey
670
+     * @return string
671
+     * @throws MultiKeyDecryptException
672
+     */
673
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
674
+        if (!$encKeyFile) {
675
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
676
+        }
677
+
678
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
679
+            return $plainContent;
680
+        } else {
681
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
682
+        }
683
+    }
684
+
685
+    /**
686
+     * @param string $plainContent
687
+     * @param array $keyFiles
688
+     * @return array
689
+     * @throws MultiKeyEncryptException
690
+     */
691
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
692
+        // openssl_seal returns false without errors if plaincontent is empty
693
+        // so trigger our own error
694
+        if (empty($plainContent)) {
695
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
696
+        }
697
+
698
+        // Set empty vars to be set by openssl by reference
699
+        $sealed = '';
700
+        $shareKeys = [];
701
+        $mappedShareKeys = [];
702
+
703
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
704
+            $i = 0;
705
+
706
+            // Ensure each shareKey is labelled with its corresponding key id
707
+            foreach ($keyFiles as $userId => $publicKey) {
708
+                $mappedShareKeys[$userId] = $shareKeys[$i];
709
+                $i++;
710
+            }
711
+
712
+            return [
713
+                'keys' => $mappedShareKeys,
714
+                'data' => $sealed
715
+            ];
716
+        } else {
717
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
718
+        }
719
+    }
720 720
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Command/ScanLegacyFormat.php 1 patch
Indentation   +101 added lines, -101 removed lines patch added patch discarded remove patch
@@ -36,105 +36,105 @@
 block discarded – undo
36 36
 
37 37
 class ScanLegacyFormat extends Command {
38 38
 
39
-	/** @var Util */
40
-	protected $util;
41
-
42
-	/** @var IConfig */
43
-	protected $config;
44
-
45
-	/** @var  QuestionHelper */
46
-	protected $questionHelper;
47
-
48
-	/** @var IUserManager */
49
-	private $userManager;
50
-
51
-	/** @var View */
52
-	private $rootView;
53
-
54
-	/**
55
-	 * @param Util $util
56
-	 * @param IConfig $config
57
-	 * @param QuestionHelper $questionHelper
58
-	 */
59
-	public function __construct(Util $util,
60
-								IConfig $config,
61
-								QuestionHelper $questionHelper,
62
-								IUserManager $userManager) {
63
-		parent::__construct();
64
-
65
-		$this->util = $util;
66
-		$this->config = $config;
67
-		$this->questionHelper = $questionHelper;
68
-		$this->userManager = $userManager;
69
-		$this->rootView = new View();
70
-	}
71
-
72
-	protected function configure() {
73
-		$this
74
-			->setName('encryption:scan:legacy-format')
75
-			->setDescription('Scan the files for the legacy format');
76
-	}
77
-
78
-	protected function execute(InputInterface $input, OutputInterface $output): int {
79
-		$result = true;
80
-
81
-		$output->writeln('Scanning all files for legacy encryption');
82
-
83
-		foreach ($this->userManager->getBackends() as $backend) {
84
-			$limit = 500;
85
-			$offset = 0;
86
-			do {
87
-				$users = $backend->getUsers('', $limit, $offset);
88
-				foreach ($users as $user) {
89
-					$output->writeln('Scanning all files for ' . $user);
90
-					$this->setupUserFS($user);
91
-					$result &= $this->scanFolder($output, '/' . $user);
92
-				}
93
-				$offset += $limit;
94
-			} while (count($users) >= $limit);
95
-		}
96
-
97
-		if ($result) {
98
-			$output->writeln('All scanned files are propperly encrypted. You can disable the legacy compatibility mode.');
99
-			return 0;
100
-		}
101
-
102
-		return 1;
103
-	}
104
-
105
-	private function scanFolder(OutputInterface $output, string $folder): bool {
106
-		$clean = true;
107
-
108
-		foreach ($this->rootView->getDirectoryContent($folder) as $item) {
109
-			$path = $folder . '/' . $item['name'];
110
-			if ($this->rootView->is_dir($path)) {
111
-				if ($this->scanFolder($output, $path) === false) {
112
-					$clean = false;
113
-				}
114
-			} else {
115
-				if (!$item->isEncrypted()) {
116
-					// ignore
117
-					continue;
118
-				}
119
-
120
-				$stats = $this->rootView->stat($path);
121
-				if (!isset($stats['hasHeader']) || $stats['hasHeader'] === false) {
122
-					$clean = false;
123
-					$output->writeln($path . ' does not have a proper header');
124
-				}
125
-			}
126
-		}
127
-
128
-		return $clean;
129
-	}
130
-
131
-	/**
132
-	 * setup user file system
133
-	 *
134
-	 * @param string $uid
135
-	 */
136
-	protected function setupUserFS($uid) {
137
-		\OC_Util::tearDownFS();
138
-		\OC_Util::setupFS($uid);
139
-	}
39
+    /** @var Util */
40
+    protected $util;
41
+
42
+    /** @var IConfig */
43
+    protected $config;
44
+
45
+    /** @var  QuestionHelper */
46
+    protected $questionHelper;
47
+
48
+    /** @var IUserManager */
49
+    private $userManager;
50
+
51
+    /** @var View */
52
+    private $rootView;
53
+
54
+    /**
55
+     * @param Util $util
56
+     * @param IConfig $config
57
+     * @param QuestionHelper $questionHelper
58
+     */
59
+    public function __construct(Util $util,
60
+                                IConfig $config,
61
+                                QuestionHelper $questionHelper,
62
+                                IUserManager $userManager) {
63
+        parent::__construct();
64
+
65
+        $this->util = $util;
66
+        $this->config = $config;
67
+        $this->questionHelper = $questionHelper;
68
+        $this->userManager = $userManager;
69
+        $this->rootView = new View();
70
+    }
71
+
72
+    protected function configure() {
73
+        $this
74
+            ->setName('encryption:scan:legacy-format')
75
+            ->setDescription('Scan the files for the legacy format');
76
+    }
77
+
78
+    protected function execute(InputInterface $input, OutputInterface $output): int {
79
+        $result = true;
80
+
81
+        $output->writeln('Scanning all files for legacy encryption');
82
+
83
+        foreach ($this->userManager->getBackends() as $backend) {
84
+            $limit = 500;
85
+            $offset = 0;
86
+            do {
87
+                $users = $backend->getUsers('', $limit, $offset);
88
+                foreach ($users as $user) {
89
+                    $output->writeln('Scanning all files for ' . $user);
90
+                    $this->setupUserFS($user);
91
+                    $result &= $this->scanFolder($output, '/' . $user);
92
+                }
93
+                $offset += $limit;
94
+            } while (count($users) >= $limit);
95
+        }
96
+
97
+        if ($result) {
98
+            $output->writeln('All scanned files are propperly encrypted. You can disable the legacy compatibility mode.');
99
+            return 0;
100
+        }
101
+
102
+        return 1;
103
+    }
104
+
105
+    private function scanFolder(OutputInterface $output, string $folder): bool {
106
+        $clean = true;
107
+
108
+        foreach ($this->rootView->getDirectoryContent($folder) as $item) {
109
+            $path = $folder . '/' . $item['name'];
110
+            if ($this->rootView->is_dir($path)) {
111
+                if ($this->scanFolder($output, $path) === false) {
112
+                    $clean = false;
113
+                }
114
+            } else {
115
+                if (!$item->isEncrypted()) {
116
+                    // ignore
117
+                    continue;
118
+                }
119
+
120
+                $stats = $this->rootView->stat($path);
121
+                if (!isset($stats['hasHeader']) || $stats['hasHeader'] === false) {
122
+                    $clean = false;
123
+                    $output->writeln($path . ' does not have a proper header');
124
+                }
125
+            }
126
+        }
127
+
128
+        return $clean;
129
+    }
130
+
131
+    /**
132
+     * setup user file system
133
+     *
134
+     * @param string $uid
135
+     */
136
+    protected function setupUserFS($uid) {
137
+        \OC_Util::tearDownFS();
138
+        \OC_Util::setupFS($uid);
139
+    }
140 140
 }
Please login to merge, or discard this patch.
apps/settings/lib/Controller/CheckSetupController.php 1 patch
Indentation   +650 added lines, -650 removed lines patch added patch discarded remove patch
@@ -74,288 +74,288 @@  discard block
 block discarded – undo
74 74
 use Symfony\Component\EventDispatcher\GenericEvent;
75 75
 
76 76
 class CheckSetupController extends Controller {
77
-	/** @var IConfig */
78
-	private $config;
79
-	/** @var IClientService */
80
-	private $clientService;
81
-	/** @var IURLGenerator */
82
-	private $urlGenerator;
83
-	/** @var IL10N */
84
-	private $l10n;
85
-	/** @var Checker */
86
-	private $checker;
87
-	/** @var ILogger */
88
-	private $logger;
89
-	/** @var EventDispatcherInterface */
90
-	private $dispatcher;
91
-	/** @var IDBConnection|Connection */
92
-	private $db;
93
-	/** @var ILockingProvider */
94
-	private $lockingProvider;
95
-	/** @var IDateTimeFormatter */
96
-	private $dateTimeFormatter;
97
-	/** @var MemoryInfo */
98
-	private $memoryInfo;
99
-	/** @var ISecureRandom */
100
-	private $secureRandom;
101
-
102
-	public function __construct($AppName,
103
-								IRequest $request,
104
-								IConfig $config,
105
-								IClientService $clientService,
106
-								IURLGenerator $urlGenerator,
107
-								IL10N $l10n,
108
-								Checker $checker,
109
-								ILogger $logger,
110
-								EventDispatcherInterface $dispatcher,
111
-								IDBConnection $db,
112
-								ILockingProvider $lockingProvider,
113
-								IDateTimeFormatter $dateTimeFormatter,
114
-								MemoryInfo $memoryInfo,
115
-								ISecureRandom $secureRandom) {
116
-		parent::__construct($AppName, $request);
117
-		$this->config = $config;
118
-		$this->clientService = $clientService;
119
-		$this->urlGenerator = $urlGenerator;
120
-		$this->l10n = $l10n;
121
-		$this->checker = $checker;
122
-		$this->logger = $logger;
123
-		$this->dispatcher = $dispatcher;
124
-		$this->db = $db;
125
-		$this->lockingProvider = $lockingProvider;
126
-		$this->dateTimeFormatter = $dateTimeFormatter;
127
-		$this->memoryInfo = $memoryInfo;
128
-		$this->secureRandom = $secureRandom;
129
-	}
130
-
131
-	/**
132
-	 * Checks if the server can connect to the internet using HTTPS and HTTP
133
-	 * @return bool
134
-	 */
135
-	private function hasInternetConnectivityProblems(): bool {
136
-		if ($this->config->getSystemValue('has_internet_connection', true) === false) {
137
-			return false;
138
-		}
139
-
140
-		$siteArray = $this->config->getSystemValue('connectivity_check_domains', [
141
-			'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org'
142
-		]);
143
-
144
-		foreach ($siteArray as $site) {
145
-			if ($this->isSiteReachable($site)) {
146
-				return false;
147
-			}
148
-		}
149
-		return true;
150
-	}
151
-
152
-	/**
153
-	 * Checks if the Nextcloud server can connect to a specific URL using both HTTPS and HTTP
154
-	 * @return bool
155
-	 */
156
-	private function isSiteReachable($sitename) {
157
-		$httpSiteName = 'http://' . $sitename . '/';
158
-		$httpsSiteName = 'https://' . $sitename . '/';
159
-
160
-		try {
161
-			$client = $this->clientService->newClient();
162
-			$client->get($httpSiteName);
163
-			$client->get($httpsSiteName);
164
-		} catch (\Exception $e) {
165
-			$this->logger->logException($e, ['app' => 'internet_connection_check']);
166
-			return false;
167
-		}
168
-		return true;
169
-	}
170
-
171
-	/**
172
-	 * Checks whether a local memcache is installed or not
173
-	 * @return bool
174
-	 */
175
-	private function isMemcacheConfigured() {
176
-		return $this->config->getSystemValue('memcache.local', null) !== null;
177
-	}
178
-
179
-	/**
180
-	 * Whether PHP can generate "secure" pseudorandom integers
181
-	 *
182
-	 * @return bool
183
-	 */
184
-	private function isRandomnessSecure() {
185
-		try {
186
-			$this->secureRandom->generate(1);
187
-		} catch (\Exception $ex) {
188
-			return false;
189
-		}
190
-		return true;
191
-	}
192
-
193
-	/**
194
-	 * Public for the sake of unit-testing
195
-	 *
196
-	 * @return array
197
-	 */
198
-	protected function getCurlVersion() {
199
-		return curl_version();
200
-	}
201
-
202
-	/**
203
-	 * Check if the used  SSL lib is outdated. Older OpenSSL and NSS versions do
204
-	 * have multiple bugs which likely lead to problems in combination with
205
-	 * functionality required by ownCloud such as SNI.
206
-	 *
207
-	 * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546
208
-	 * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172
209
-	 * @return string
210
-	 */
211
-	private function isUsedTlsLibOutdated() {
212
-		// Don't run check when:
213
-		// 1. Server has `has_internet_connection` set to false
214
-		// 2. AppStore AND S2S is disabled
215
-		if (!$this->config->getSystemValue('has_internet_connection', true)) {
216
-			return '';
217
-		}
218
-		if (!$this->config->getSystemValue('appstoreenabled', true)
219
-			&& $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no'
220
-			&& $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') {
221
-			return '';
222
-		}
223
-
224
-		$versionString = $this->getCurlVersion();
225
-		if (isset($versionString['ssl_version'])) {
226
-			$versionString = $versionString['ssl_version'];
227
-		} else {
228
-			return '';
229
-		}
230
-
231
-		$features = (string)$this->l10n->t('installing and updating apps via the app store or Federated Cloud Sharing');
232
-		if (!$this->config->getSystemValue('appstoreenabled', true)) {
233
-			$features = (string)$this->l10n->t('Federated Cloud Sharing');
234
-		}
235
-
236
-		// Check if at least OpenSSL after 1.01d or 1.0.2b
237
-		if (strpos($versionString, 'OpenSSL/') === 0) {
238
-			$majorVersion = substr($versionString, 8, 5);
239
-			$patchRelease = substr($versionString, 13, 6);
240
-
241
-			if (($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) ||
242
-				($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) {
243
-				return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['OpenSSL', $versionString, $features]);
244
-			}
245
-		}
246
-
247
-		// Check if NSS and perform heuristic check
248
-		if (strpos($versionString, 'NSS/') === 0) {
249
-			try {
250
-				$firstClient = $this->clientService->newClient();
251
-				$firstClient->get('https://nextcloud.com/');
252
-
253
-				$secondClient = $this->clientService->newClient();
254
-				$secondClient->get('https://nextcloud.com/');
255
-			} catch (ClientException $e) {
256
-				if ($e->getResponse()->getStatusCode() === 400) {
257
-					return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['NSS', $versionString, $features]);
258
-				}
259
-			}
260
-		}
261
-
262
-		return '';
263
-	}
264
-
265
-	/**
266
-	 * Whether the version is outdated
267
-	 *
268
-	 * @return bool
269
-	 */
270
-	protected function isPhpOutdated(): bool {
271
-		return PHP_VERSION_ID < 70300;
272
-	}
273
-
274
-	/**
275
-	 * Whether the php version is still supported (at time of release)
276
-	 * according to: https://secure.php.net/supported-versions.php
277
-	 *
278
-	 * @return array
279
-	 */
280
-	private function isPhpSupported(): array {
281
-		return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION];
282
-	}
283
-
284
-	/**
285
-	 * Check if the reverse proxy configuration is working as expected
286
-	 *
287
-	 * @return bool
288
-	 */
289
-	private function forwardedForHeadersWorking() {
290
-		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
291
-		$remoteAddress = $this->request->getHeader('REMOTE_ADDR');
292
-
293
-		if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') {
294
-			return false;
295
-		}
296
-
297
-		if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies, true)) {
298
-			return $remoteAddress !== $this->request->getRemoteAddress();
299
-		}
300
-
301
-		// either not enabled or working correctly
302
-		return true;
303
-	}
304
-
305
-	/**
306
-	 * Checks if the correct memcache module for PHP is installed. Only
307
-	 * fails if memcached is configured and the working module is not installed.
308
-	 *
309
-	 * @return bool
310
-	 */
311
-	private function isCorrectMemcachedPHPModuleInstalled() {
312
-		if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') {
313
-			return true;
314
-		}
315
-
316
-		// there are two different memcached modules for PHP
317
-		// we only support memcached and not memcache
318
-		// https://code.google.com/p/memcached/wiki/PHPClientComparison
319
-		return !(!extension_loaded('memcached') && extension_loaded('memcache'));
320
-	}
321
-
322
-	/**
323
-	 * Checks if set_time_limit is not disabled.
324
-	 *
325
-	 * @return bool
326
-	 */
327
-	private function isSettimelimitAvailable() {
328
-		if (function_exists('set_time_limit')
329
-			&& strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
330
-			return true;
331
-		}
332
-
333
-		return false;
334
-	}
335
-
336
-	/**
337
-	 * @return RedirectResponse
338
-	 */
339
-	public function rescanFailedIntegrityCheck() {
340
-		$this->checker->runInstanceVerification();
341
-		return new RedirectResponse(
342
-			$this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview'])
343
-		);
344
-	}
345
-
346
-	/**
347
-	 * @NoCSRFRequired
348
-	 * @return DataResponse
349
-	 */
350
-	public function getFailedIntegrityCheckFiles() {
351
-		if (!$this->checker->isCodeCheckEnforced()) {
352
-			return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.');
353
-		}
354
-
355
-		$completeResults = $this->checker->getResults();
356
-
357
-		if (!empty($completeResults)) {
358
-			$formattedTextResponse = 'Technical information
77
+    /** @var IConfig */
78
+    private $config;
79
+    /** @var IClientService */
80
+    private $clientService;
81
+    /** @var IURLGenerator */
82
+    private $urlGenerator;
83
+    /** @var IL10N */
84
+    private $l10n;
85
+    /** @var Checker */
86
+    private $checker;
87
+    /** @var ILogger */
88
+    private $logger;
89
+    /** @var EventDispatcherInterface */
90
+    private $dispatcher;
91
+    /** @var IDBConnection|Connection */
92
+    private $db;
93
+    /** @var ILockingProvider */
94
+    private $lockingProvider;
95
+    /** @var IDateTimeFormatter */
96
+    private $dateTimeFormatter;
97
+    /** @var MemoryInfo */
98
+    private $memoryInfo;
99
+    /** @var ISecureRandom */
100
+    private $secureRandom;
101
+
102
+    public function __construct($AppName,
103
+                                IRequest $request,
104
+                                IConfig $config,
105
+                                IClientService $clientService,
106
+                                IURLGenerator $urlGenerator,
107
+                                IL10N $l10n,
108
+                                Checker $checker,
109
+                                ILogger $logger,
110
+                                EventDispatcherInterface $dispatcher,
111
+                                IDBConnection $db,
112
+                                ILockingProvider $lockingProvider,
113
+                                IDateTimeFormatter $dateTimeFormatter,
114
+                                MemoryInfo $memoryInfo,
115
+                                ISecureRandom $secureRandom) {
116
+        parent::__construct($AppName, $request);
117
+        $this->config = $config;
118
+        $this->clientService = $clientService;
119
+        $this->urlGenerator = $urlGenerator;
120
+        $this->l10n = $l10n;
121
+        $this->checker = $checker;
122
+        $this->logger = $logger;
123
+        $this->dispatcher = $dispatcher;
124
+        $this->db = $db;
125
+        $this->lockingProvider = $lockingProvider;
126
+        $this->dateTimeFormatter = $dateTimeFormatter;
127
+        $this->memoryInfo = $memoryInfo;
128
+        $this->secureRandom = $secureRandom;
129
+    }
130
+
131
+    /**
132
+     * Checks if the server can connect to the internet using HTTPS and HTTP
133
+     * @return bool
134
+     */
135
+    private function hasInternetConnectivityProblems(): bool {
136
+        if ($this->config->getSystemValue('has_internet_connection', true) === false) {
137
+            return false;
138
+        }
139
+
140
+        $siteArray = $this->config->getSystemValue('connectivity_check_domains', [
141
+            'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org'
142
+        ]);
143
+
144
+        foreach ($siteArray as $site) {
145
+            if ($this->isSiteReachable($site)) {
146
+                return false;
147
+            }
148
+        }
149
+        return true;
150
+    }
151
+
152
+    /**
153
+     * Checks if the Nextcloud server can connect to a specific URL using both HTTPS and HTTP
154
+     * @return bool
155
+     */
156
+    private function isSiteReachable($sitename) {
157
+        $httpSiteName = 'http://' . $sitename . '/';
158
+        $httpsSiteName = 'https://' . $sitename . '/';
159
+
160
+        try {
161
+            $client = $this->clientService->newClient();
162
+            $client->get($httpSiteName);
163
+            $client->get($httpsSiteName);
164
+        } catch (\Exception $e) {
165
+            $this->logger->logException($e, ['app' => 'internet_connection_check']);
166
+            return false;
167
+        }
168
+        return true;
169
+    }
170
+
171
+    /**
172
+     * Checks whether a local memcache is installed or not
173
+     * @return bool
174
+     */
175
+    private function isMemcacheConfigured() {
176
+        return $this->config->getSystemValue('memcache.local', null) !== null;
177
+    }
178
+
179
+    /**
180
+     * Whether PHP can generate "secure" pseudorandom integers
181
+     *
182
+     * @return bool
183
+     */
184
+    private function isRandomnessSecure() {
185
+        try {
186
+            $this->secureRandom->generate(1);
187
+        } catch (\Exception $ex) {
188
+            return false;
189
+        }
190
+        return true;
191
+    }
192
+
193
+    /**
194
+     * Public for the sake of unit-testing
195
+     *
196
+     * @return array
197
+     */
198
+    protected function getCurlVersion() {
199
+        return curl_version();
200
+    }
201
+
202
+    /**
203
+     * Check if the used  SSL lib is outdated. Older OpenSSL and NSS versions do
204
+     * have multiple bugs which likely lead to problems in combination with
205
+     * functionality required by ownCloud such as SNI.
206
+     *
207
+     * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546
208
+     * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172
209
+     * @return string
210
+     */
211
+    private function isUsedTlsLibOutdated() {
212
+        // Don't run check when:
213
+        // 1. Server has `has_internet_connection` set to false
214
+        // 2. AppStore AND S2S is disabled
215
+        if (!$this->config->getSystemValue('has_internet_connection', true)) {
216
+            return '';
217
+        }
218
+        if (!$this->config->getSystemValue('appstoreenabled', true)
219
+            && $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no'
220
+            && $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') {
221
+            return '';
222
+        }
223
+
224
+        $versionString = $this->getCurlVersion();
225
+        if (isset($versionString['ssl_version'])) {
226
+            $versionString = $versionString['ssl_version'];
227
+        } else {
228
+            return '';
229
+        }
230
+
231
+        $features = (string)$this->l10n->t('installing and updating apps via the app store or Federated Cloud Sharing');
232
+        if (!$this->config->getSystemValue('appstoreenabled', true)) {
233
+            $features = (string)$this->l10n->t('Federated Cloud Sharing');
234
+        }
235
+
236
+        // Check if at least OpenSSL after 1.01d or 1.0.2b
237
+        if (strpos($versionString, 'OpenSSL/') === 0) {
238
+            $majorVersion = substr($versionString, 8, 5);
239
+            $patchRelease = substr($versionString, 13, 6);
240
+
241
+            if (($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) ||
242
+                ($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) {
243
+                return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['OpenSSL', $versionString, $features]);
244
+            }
245
+        }
246
+
247
+        // Check if NSS and perform heuristic check
248
+        if (strpos($versionString, 'NSS/') === 0) {
249
+            try {
250
+                $firstClient = $this->clientService->newClient();
251
+                $firstClient->get('https://nextcloud.com/');
252
+
253
+                $secondClient = $this->clientService->newClient();
254
+                $secondClient->get('https://nextcloud.com/');
255
+            } catch (ClientException $e) {
256
+                if ($e->getResponse()->getStatusCode() === 400) {
257
+                    return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['NSS', $versionString, $features]);
258
+                }
259
+            }
260
+        }
261
+
262
+        return '';
263
+    }
264
+
265
+    /**
266
+     * Whether the version is outdated
267
+     *
268
+     * @return bool
269
+     */
270
+    protected function isPhpOutdated(): bool {
271
+        return PHP_VERSION_ID < 70300;
272
+    }
273
+
274
+    /**
275
+     * Whether the php version is still supported (at time of release)
276
+     * according to: https://secure.php.net/supported-versions.php
277
+     *
278
+     * @return array
279
+     */
280
+    private function isPhpSupported(): array {
281
+        return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION];
282
+    }
283
+
284
+    /**
285
+     * Check if the reverse proxy configuration is working as expected
286
+     *
287
+     * @return bool
288
+     */
289
+    private function forwardedForHeadersWorking() {
290
+        $trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
291
+        $remoteAddress = $this->request->getHeader('REMOTE_ADDR');
292
+
293
+        if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') {
294
+            return false;
295
+        }
296
+
297
+        if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies, true)) {
298
+            return $remoteAddress !== $this->request->getRemoteAddress();
299
+        }
300
+
301
+        // either not enabled or working correctly
302
+        return true;
303
+    }
304
+
305
+    /**
306
+     * Checks if the correct memcache module for PHP is installed. Only
307
+     * fails if memcached is configured and the working module is not installed.
308
+     *
309
+     * @return bool
310
+     */
311
+    private function isCorrectMemcachedPHPModuleInstalled() {
312
+        if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') {
313
+            return true;
314
+        }
315
+
316
+        // there are two different memcached modules for PHP
317
+        // we only support memcached and not memcache
318
+        // https://code.google.com/p/memcached/wiki/PHPClientComparison
319
+        return !(!extension_loaded('memcached') && extension_loaded('memcache'));
320
+    }
321
+
322
+    /**
323
+     * Checks if set_time_limit is not disabled.
324
+     *
325
+     * @return bool
326
+     */
327
+    private function isSettimelimitAvailable() {
328
+        if (function_exists('set_time_limit')
329
+            && strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
330
+            return true;
331
+        }
332
+
333
+        return false;
334
+    }
335
+
336
+    /**
337
+     * @return RedirectResponse
338
+     */
339
+    public function rescanFailedIntegrityCheck() {
340
+        $this->checker->runInstanceVerification();
341
+        return new RedirectResponse(
342
+            $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview'])
343
+        );
344
+    }
345
+
346
+    /**
347
+     * @NoCSRFRequired
348
+     * @return DataResponse
349
+     */
350
+    public function getFailedIntegrityCheckFiles() {
351
+        if (!$this->checker->isCodeCheckEnforced()) {
352
+            return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.');
353
+        }
354
+
355
+        $completeResults = $this->checker->getResults();
356
+
357
+        if (!empty($completeResults)) {
358
+            $formattedTextResponse = 'Technical information
359 359
 =====================
360 360
 The following list covers which files have failed the integrity check. Please read
361 361
 the previous linked documentation to learn more about the errors and how to fix
@@ -364,375 +364,375 @@  discard block
 block discarded – undo
364 364
 Results
365 365
 =======
366 366
 ';
367
-			foreach ($completeResults as $context => $contextResult) {
368
-				$formattedTextResponse .= "- $context\n";
369
-
370
-				foreach ($contextResult as $category => $result) {
371
-					$formattedTextResponse .= "\t- $category\n";
372
-					if ($category !== 'EXCEPTION') {
373
-						foreach ($result as $key => $results) {
374
-							$formattedTextResponse .= "\t\t- $key\n";
375
-						}
376
-					} else {
377
-						foreach ($result as $key => $results) {
378
-							$formattedTextResponse .= "\t\t- $results\n";
379
-						}
380
-					}
381
-				}
382
-			}
383
-
384
-			$formattedTextResponse .= '
367
+            foreach ($completeResults as $context => $contextResult) {
368
+                $formattedTextResponse .= "- $context\n";
369
+
370
+                foreach ($contextResult as $category => $result) {
371
+                    $formattedTextResponse .= "\t- $category\n";
372
+                    if ($category !== 'EXCEPTION') {
373
+                        foreach ($result as $key => $results) {
374
+                            $formattedTextResponse .= "\t\t- $key\n";
375
+                        }
376
+                    } else {
377
+                        foreach ($result as $key => $results) {
378
+                            $formattedTextResponse .= "\t\t- $results\n";
379
+                        }
380
+                    }
381
+                }
382
+            }
383
+
384
+            $formattedTextResponse .= '
385 385
 Raw output
386 386
 ==========
387 387
 ';
388
-			$formattedTextResponse .= print_r($completeResults, true);
389
-		} else {
390
-			$formattedTextResponse = 'No errors have been found.';
391
-		}
392
-
393
-
394
-		$response = new DataDisplayResponse(
395
-			$formattedTextResponse,
396
-			Http::STATUS_OK,
397
-			[
398
-				'Content-Type' => 'text/plain',
399
-			]
400
-		);
401
-
402
-		return $response;
403
-	}
404
-
405
-	/**
406
-	 * Checks whether a PHP opcache is properly set up
407
-	 * @return bool
408
-	 */
409
-	protected function isOpcacheProperlySetup() {
410
-		$iniWrapper = new IniGetWrapper();
411
-
412
-		if (!$iniWrapper->getBool('opcache.enable')) {
413
-			return false;
414
-		}
415
-
416
-		if (!$iniWrapper->getBool('opcache.save_comments')) {
417
-			return false;
418
-		}
419
-
420
-		if ($iniWrapper->getNumeric('opcache.max_accelerated_files') < 10000) {
421
-			return false;
422
-		}
423
-
424
-		if ($iniWrapper->getNumeric('opcache.memory_consumption') < 128) {
425
-			return false;
426
-		}
427
-
428
-		if ($iniWrapper->getNumeric('opcache.interned_strings_buffer') < 8) {
429
-			return false;
430
-		}
431
-
432
-		return true;
433
-	}
434
-
435
-	/**
436
-	 * Check if the required FreeType functions are present
437
-	 * @return bool
438
-	 */
439
-	protected function hasFreeTypeSupport() {
440
-		return function_exists('imagettfbbox') && function_exists('imagettftext');
441
-	}
442
-
443
-	protected function hasMissingIndexes(): array {
444
-		$indexInfo = new MissingIndexInformation();
445
-		// Dispatch event so apps can also hint for pending index updates if needed
446
-		$event = new GenericEvent($indexInfo);
447
-		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);
448
-
449
-		return $indexInfo->getListOfMissingIndexes();
450
-	}
451
-
452
-	protected function hasMissingColumns(): array {
453
-		$indexInfo = new MissingColumnInformation();
454
-		// Dispatch event so apps can also hint for pending index updates if needed
455
-		$event = new GenericEvent($indexInfo);
456
-		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event);
457
-
458
-		return $indexInfo->getListOfMissingColumns();
459
-	}
460
-
461
-	protected function isSqliteUsed() {
462
-		return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false;
463
-	}
464
-
465
-	protected function isReadOnlyConfig(): bool {
466
-		return \OC_Helper::isReadOnlyConfigEnabled();
467
-	}
468
-
469
-	protected function hasValidTransactionIsolationLevel(): bool {
470
-		try {
471
-			if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) {
472
-				return true;
473
-			}
474
-
475
-			return $this->db->getTransactionIsolation() === Connection::TRANSACTION_READ_COMMITTED;
476
-		} catch (DBALException $e) {
477
-			// ignore
478
-		}
479
-
480
-		return true;
481
-	}
482
-
483
-	protected function hasFileinfoInstalled(): bool {
484
-		return \OC_Util::fileInfoLoaded();
485
-	}
486
-
487
-	protected function hasWorkingFileLocking(): bool {
488
-		return !($this->lockingProvider instanceof NoopLockingProvider);
489
-	}
490
-
491
-	protected function getSuggestedOverwriteCliURL(): string {
492
-		$suggestedOverwriteCliUrl = '';
493
-		if ($this->config->getSystemValue('overwrite.cli.url', '') === '') {
494
-			$suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT;
495
-			if (!$this->config->getSystemValue('config_is_read_only', false)) {
496
-				// Set the overwrite URL when it was not set yet.
497
-				$this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl);
498
-				$suggestedOverwriteCliUrl = '';
499
-			}
500
-		}
501
-		return $suggestedOverwriteCliUrl;
502
-	}
503
-
504
-	protected function getLastCronInfo(): array {
505
-		$lastCronRun = $this->config->getAppValue('core', 'lastcron', 0);
506
-		return [
507
-			'diffInSeconds' => time() - $lastCronRun,
508
-			'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun),
509
-			'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs',
510
-		];
511
-	}
512
-
513
-	protected function getCronErrors() {
514
-		$errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true);
515
-
516
-		if (is_array($errors)) {
517
-			return $errors;
518
-		}
519
-
520
-		return [];
521
-	}
522
-
523
-	protected function isPHPMailerUsed(): bool {
524
-		return $this->config->getSystemValue('mail_smtpmode', 'smtp') === 'php';
525
-	}
526
-
527
-	protected function hasOpcacheLoaded(): bool {
528
-		return extension_loaded('Zend OPcache');
529
-	}
530
-
531
-	/**
532
-	 * Iterates through the configured app roots and
533
-	 * tests if the subdirectories are owned by the same user than the current user.
534
-	 *
535
-	 * @return array
536
-	 */
537
-	protected function getAppDirsWithDifferentOwner(): array {
538
-		$currentUser = posix_getuid();
539
-		$appDirsWithDifferentOwner = [[]];
540
-
541
-		foreach (OC::$APPSROOTS as $appRoot) {
542
-			if ($appRoot['writable'] === true) {
543
-				$appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot);
544
-			}
545
-		}
546
-
547
-		$appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner);
548
-		sort($appDirsWithDifferentOwner);
549
-
550
-		return $appDirsWithDifferentOwner;
551
-	}
552
-
553
-	/**
554
-	 * Tests if the directories for one apps directory are writable by the current user.
555
-	 *
556
-	 * @param int $currentUser The current user
557
-	 * @param array $appRoot The app root config
558
-	 * @return string[] The none writable directory paths inside the app root
559
-	 */
560
-	private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array {
561
-		$appDirsWithDifferentOwner = [];
562
-		$appsPath = $appRoot['path'];
563
-		$appsDir = new DirectoryIterator($appRoot['path']);
564
-
565
-		foreach ($appsDir as $fileInfo) {
566
-			if ($fileInfo->isDir() && !$fileInfo->isDot()) {
567
-				$absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
568
-				$appDirUser = fileowner($absAppPath);
569
-				if ($appDirUser !== $currentUser) {
570
-					$appDirsWithDifferentOwner[] = $absAppPath;
571
-				}
572
-			}
573
-		}
574
-
575
-		return $appDirsWithDifferentOwner;
576
-	}
577
-
578
-	/**
579
-	 * Checks for potential PHP modules that would improve the instance
580
-	 *
581
-	 * @return string[] A list of PHP modules that is recommended
582
-	 */
583
-	protected function hasRecommendedPHPModules(): array {
584
-		$recommendedPHPModules = [];
585
-
586
-		if (!extension_loaded('intl')) {
587
-			$recommendedPHPModules[] = 'intl';
588
-		}
589
-
590
-		if (!extension_loaded('bcmath')) {
591
-			$recommendedPHPModules[] = 'bcmath';
592
-		}
593
-
594
-		if (!extension_loaded('gmp')) {
595
-			$recommendedPHPModules[] = 'gmp';
596
-		}
597
-
598
-		if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') {
599
-			if (!extension_loaded('imagick')) {
600
-				$recommendedPHPModules[] = 'imagick';
601
-			}
602
-		}
603
-
604
-		return $recommendedPHPModules;
605
-	}
606
-
607
-	protected function isMysqlUsedWithoutUTF8MB4(): bool {
608
-		return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false);
609
-	}
610
-
611
-	protected function hasBigIntConversionPendingColumns(): array {
612
-		// copy of ConvertFilecacheBigInt::getColumnsByTable()
613
-		$tables = [
614
-			'activity' => ['activity_id', 'object_id'],
615
-			'activity_mq' => ['mail_id'],
616
-			'authtoken' => ['id'],
617
-			'bruteforce_attempts' => ['id'],
618
-			'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'],
619
-			'file_locks' => ['id'],
620
-			'jobs' => ['id'],
621
-			'mimetypes' => ['id'],
622
-			'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'],
623
-			'storages' => ['numeric_id'],
624
-		];
625
-
626
-		$schema = new SchemaWrapper($this->db);
627
-		$isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform;
628
-		$pendingColumns = [];
629
-
630
-		foreach ($tables as $tableName => $columns) {
631
-			if (!$schema->hasTable($tableName)) {
632
-				continue;
633
-			}
634
-
635
-			$table = $schema->getTable($tableName);
636
-			foreach ($columns as $columnName) {
637
-				$column = $table->getColumn($columnName);
638
-				$isAutoIncrement = $column->getAutoincrement();
639
-				$isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement;
640
-				if ($column->getType()->getName() !== Types::BIGINT && !$isAutoIncrementOnSqlite) {
641
-					$pendingColumns[] = $tableName . '.' . $columnName;
642
-				}
643
-			}
644
-		}
645
-
646
-		return $pendingColumns;
647
-	}
648
-
649
-	protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool {
650
-		$objectStore = $this->config->getSystemValue('objectstore', null);
651
-		$objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null);
652
-
653
-		if (!isset($objectStoreMultibucket) && !isset($objectStore)) {
654
-			return true;
655
-		}
656
-
657
-		if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') {
658
-			return true;
659
-		}
660
-
661
-		if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') {
662
-			return true;
663
-		}
664
-
665
-		$tempPath = sys_get_temp_dir();
666
-		if (!is_dir($tempPath)) {
667
-			$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
668
-			return false;
669
-		}
670
-		$freeSpaceInTemp = disk_free_space($tempPath);
671
-		if ($freeSpaceInTemp === false) {
672
-			$this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
673
-			return false;
674
-		}
675
-
676
-		$freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024;
677
-		if ($freeSpaceInTempInGB > 50) {
678
-			return true;
679
-		}
680
-
681
-		$this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath);
682
-		return false;
683
-	}
684
-
685
-	/**
686
-	 * @return DataResponse
687
-	 */
688
-	public function check() {
689
-		$phpDefaultCharset = new PhpDefaultCharset();
690
-		$phpOutputBuffering = new PhpOutputBuffering();
691
-		$legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator);
692
-		return new DataResponse(
693
-			[
694
-				'isGetenvServerWorking' => !empty(getenv('PATH')),
695
-				'isReadOnlyConfig' => $this->isReadOnlyConfig(),
696
-				'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(),
697
-				'hasFileinfoInstalled' => $this->hasFileinfoInstalled(),
698
-				'hasWorkingFileLocking' => $this->hasWorkingFileLocking(),
699
-				'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(),
700
-				'cronInfo' => $this->getLastCronInfo(),
701
-				'cronErrors' => $this->getCronErrors(),
702
-				'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(),
703
-				'isMemcacheConfigured' => $this->isMemcacheConfigured(),
704
-				'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'),
705
-				'isRandomnessSecure' => $this->isRandomnessSecure(),
706
-				'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'),
707
-				'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(),
708
-				'phpSupported' => $this->isPhpSupported(),
709
-				'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(),
710
-				'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'),
711
-				'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(),
712
-				'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(),
713
-				'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'),
714
-				'isOpcacheProperlySetup' => $this->isOpcacheProperlySetup(),
715
-				'hasOpcacheLoaded' => $this->hasOpcacheLoaded(),
716
-				'phpOpcacheDocumentation' => $this->urlGenerator->linkToDocs('admin-php-opcache'),
717
-				'isSettimelimitAvailable' => $this->isSettimelimitAvailable(),
718
-				'hasFreeTypeSupport' => $this->hasFreeTypeSupport(),
719
-				'missingIndexes' => $this->hasMissingIndexes(),
720
-				'missingColumns' => $this->hasMissingColumns(),
721
-				'isSqliteUsed' => $this->isSqliteUsed(),
722
-				'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'),
723
-				'isPHPMailerUsed' => $this->isPHPMailerUsed(),
724
-				'mailSettingsDocumentation' => $this->urlGenerator->getAbsoluteURL('index.php/settings/admin'),
725
-				'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(),
726
-				'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(),
727
-				'recommendedPHPModules' => $this->hasRecommendedPHPModules(),
728
-				'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(),
729
-				'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(),
730
-				'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(),
731
-				'reverseProxyGeneratedURL' => $this->urlGenerator->getAbsoluteURL('index.php'),
732
-				PhpDefaultCharset::class => ['pass' => $phpDefaultCharset->run(), 'description' => $phpDefaultCharset->description(), 'severity' => $phpDefaultCharset->severity()],
733
-				PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()],
734
-				LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()],
735
-			]
736
-		);
737
-	}
388
+            $formattedTextResponse .= print_r($completeResults, true);
389
+        } else {
390
+            $formattedTextResponse = 'No errors have been found.';
391
+        }
392
+
393
+
394
+        $response = new DataDisplayResponse(
395
+            $formattedTextResponse,
396
+            Http::STATUS_OK,
397
+            [
398
+                'Content-Type' => 'text/plain',
399
+            ]
400
+        );
401
+
402
+        return $response;
403
+    }
404
+
405
+    /**
406
+     * Checks whether a PHP opcache is properly set up
407
+     * @return bool
408
+     */
409
+    protected function isOpcacheProperlySetup() {
410
+        $iniWrapper = new IniGetWrapper();
411
+
412
+        if (!$iniWrapper->getBool('opcache.enable')) {
413
+            return false;
414
+        }
415
+
416
+        if (!$iniWrapper->getBool('opcache.save_comments')) {
417
+            return false;
418
+        }
419
+
420
+        if ($iniWrapper->getNumeric('opcache.max_accelerated_files') < 10000) {
421
+            return false;
422
+        }
423
+
424
+        if ($iniWrapper->getNumeric('opcache.memory_consumption') < 128) {
425
+            return false;
426
+        }
427
+
428
+        if ($iniWrapper->getNumeric('opcache.interned_strings_buffer') < 8) {
429
+            return false;
430
+        }
431
+
432
+        return true;
433
+    }
434
+
435
+    /**
436
+     * Check if the required FreeType functions are present
437
+     * @return bool
438
+     */
439
+    protected function hasFreeTypeSupport() {
440
+        return function_exists('imagettfbbox') && function_exists('imagettftext');
441
+    }
442
+
443
+    protected function hasMissingIndexes(): array {
444
+        $indexInfo = new MissingIndexInformation();
445
+        // Dispatch event so apps can also hint for pending index updates if needed
446
+        $event = new GenericEvent($indexInfo);
447
+        $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);
448
+
449
+        return $indexInfo->getListOfMissingIndexes();
450
+    }
451
+
452
+    protected function hasMissingColumns(): array {
453
+        $indexInfo = new MissingColumnInformation();
454
+        // Dispatch event so apps can also hint for pending index updates if needed
455
+        $event = new GenericEvent($indexInfo);
456
+        $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event);
457
+
458
+        return $indexInfo->getListOfMissingColumns();
459
+    }
460
+
461
+    protected function isSqliteUsed() {
462
+        return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false;
463
+    }
464
+
465
+    protected function isReadOnlyConfig(): bool {
466
+        return \OC_Helper::isReadOnlyConfigEnabled();
467
+    }
468
+
469
+    protected function hasValidTransactionIsolationLevel(): bool {
470
+        try {
471
+            if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) {
472
+                return true;
473
+            }
474
+
475
+            return $this->db->getTransactionIsolation() === Connection::TRANSACTION_READ_COMMITTED;
476
+        } catch (DBALException $e) {
477
+            // ignore
478
+        }
479
+
480
+        return true;
481
+    }
482
+
483
+    protected function hasFileinfoInstalled(): bool {
484
+        return \OC_Util::fileInfoLoaded();
485
+    }
486
+
487
+    protected function hasWorkingFileLocking(): bool {
488
+        return !($this->lockingProvider instanceof NoopLockingProvider);
489
+    }
490
+
491
+    protected function getSuggestedOverwriteCliURL(): string {
492
+        $suggestedOverwriteCliUrl = '';
493
+        if ($this->config->getSystemValue('overwrite.cli.url', '') === '') {
494
+            $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT;
495
+            if (!$this->config->getSystemValue('config_is_read_only', false)) {
496
+                // Set the overwrite URL when it was not set yet.
497
+                $this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl);
498
+                $suggestedOverwriteCliUrl = '';
499
+            }
500
+        }
501
+        return $suggestedOverwriteCliUrl;
502
+    }
503
+
504
+    protected function getLastCronInfo(): array {
505
+        $lastCronRun = $this->config->getAppValue('core', 'lastcron', 0);
506
+        return [
507
+            'diffInSeconds' => time() - $lastCronRun,
508
+            'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun),
509
+            'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs',
510
+        ];
511
+    }
512
+
513
+    protected function getCronErrors() {
514
+        $errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true);
515
+
516
+        if (is_array($errors)) {
517
+            return $errors;
518
+        }
519
+
520
+        return [];
521
+    }
522
+
523
+    protected function isPHPMailerUsed(): bool {
524
+        return $this->config->getSystemValue('mail_smtpmode', 'smtp') === 'php';
525
+    }
526
+
527
+    protected function hasOpcacheLoaded(): bool {
528
+        return extension_loaded('Zend OPcache');
529
+    }
530
+
531
+    /**
532
+     * Iterates through the configured app roots and
533
+     * tests if the subdirectories are owned by the same user than the current user.
534
+     *
535
+     * @return array
536
+     */
537
+    protected function getAppDirsWithDifferentOwner(): array {
538
+        $currentUser = posix_getuid();
539
+        $appDirsWithDifferentOwner = [[]];
540
+
541
+        foreach (OC::$APPSROOTS as $appRoot) {
542
+            if ($appRoot['writable'] === true) {
543
+                $appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot);
544
+            }
545
+        }
546
+
547
+        $appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner);
548
+        sort($appDirsWithDifferentOwner);
549
+
550
+        return $appDirsWithDifferentOwner;
551
+    }
552
+
553
+    /**
554
+     * Tests if the directories for one apps directory are writable by the current user.
555
+     *
556
+     * @param int $currentUser The current user
557
+     * @param array $appRoot The app root config
558
+     * @return string[] The none writable directory paths inside the app root
559
+     */
560
+    private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array {
561
+        $appDirsWithDifferentOwner = [];
562
+        $appsPath = $appRoot['path'];
563
+        $appsDir = new DirectoryIterator($appRoot['path']);
564
+
565
+        foreach ($appsDir as $fileInfo) {
566
+            if ($fileInfo->isDir() && !$fileInfo->isDot()) {
567
+                $absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
568
+                $appDirUser = fileowner($absAppPath);
569
+                if ($appDirUser !== $currentUser) {
570
+                    $appDirsWithDifferentOwner[] = $absAppPath;
571
+                }
572
+            }
573
+        }
574
+
575
+        return $appDirsWithDifferentOwner;
576
+    }
577
+
578
+    /**
579
+     * Checks for potential PHP modules that would improve the instance
580
+     *
581
+     * @return string[] A list of PHP modules that is recommended
582
+     */
583
+    protected function hasRecommendedPHPModules(): array {
584
+        $recommendedPHPModules = [];
585
+
586
+        if (!extension_loaded('intl')) {
587
+            $recommendedPHPModules[] = 'intl';
588
+        }
589
+
590
+        if (!extension_loaded('bcmath')) {
591
+            $recommendedPHPModules[] = 'bcmath';
592
+        }
593
+
594
+        if (!extension_loaded('gmp')) {
595
+            $recommendedPHPModules[] = 'gmp';
596
+        }
597
+
598
+        if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') {
599
+            if (!extension_loaded('imagick')) {
600
+                $recommendedPHPModules[] = 'imagick';
601
+            }
602
+        }
603
+
604
+        return $recommendedPHPModules;
605
+    }
606
+
607
+    protected function isMysqlUsedWithoutUTF8MB4(): bool {
608
+        return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false);
609
+    }
610
+
611
+    protected function hasBigIntConversionPendingColumns(): array {
612
+        // copy of ConvertFilecacheBigInt::getColumnsByTable()
613
+        $tables = [
614
+            'activity' => ['activity_id', 'object_id'],
615
+            'activity_mq' => ['mail_id'],
616
+            'authtoken' => ['id'],
617
+            'bruteforce_attempts' => ['id'],
618
+            'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'],
619
+            'file_locks' => ['id'],
620
+            'jobs' => ['id'],
621
+            'mimetypes' => ['id'],
622
+            'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'],
623
+            'storages' => ['numeric_id'],
624
+        ];
625
+
626
+        $schema = new SchemaWrapper($this->db);
627
+        $isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform;
628
+        $pendingColumns = [];
629
+
630
+        foreach ($tables as $tableName => $columns) {
631
+            if (!$schema->hasTable($tableName)) {
632
+                continue;
633
+            }
634
+
635
+            $table = $schema->getTable($tableName);
636
+            foreach ($columns as $columnName) {
637
+                $column = $table->getColumn($columnName);
638
+                $isAutoIncrement = $column->getAutoincrement();
639
+                $isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement;
640
+                if ($column->getType()->getName() !== Types::BIGINT && !$isAutoIncrementOnSqlite) {
641
+                    $pendingColumns[] = $tableName . '.' . $columnName;
642
+                }
643
+            }
644
+        }
645
+
646
+        return $pendingColumns;
647
+    }
648
+
649
+    protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool {
650
+        $objectStore = $this->config->getSystemValue('objectstore', null);
651
+        $objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null);
652
+
653
+        if (!isset($objectStoreMultibucket) && !isset($objectStore)) {
654
+            return true;
655
+        }
656
+
657
+        if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') {
658
+            return true;
659
+        }
660
+
661
+        if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') {
662
+            return true;
663
+        }
664
+
665
+        $tempPath = sys_get_temp_dir();
666
+        if (!is_dir($tempPath)) {
667
+            $this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
668
+            return false;
669
+        }
670
+        $freeSpaceInTemp = disk_free_space($tempPath);
671
+        if ($freeSpaceInTemp === false) {
672
+            $this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
673
+            return false;
674
+        }
675
+
676
+        $freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024;
677
+        if ($freeSpaceInTempInGB > 50) {
678
+            return true;
679
+        }
680
+
681
+        $this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath);
682
+        return false;
683
+    }
684
+
685
+    /**
686
+     * @return DataResponse
687
+     */
688
+    public function check() {
689
+        $phpDefaultCharset = new PhpDefaultCharset();
690
+        $phpOutputBuffering = new PhpOutputBuffering();
691
+        $legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator);
692
+        return new DataResponse(
693
+            [
694
+                'isGetenvServerWorking' => !empty(getenv('PATH')),
695
+                'isReadOnlyConfig' => $this->isReadOnlyConfig(),
696
+                'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(),
697
+                'hasFileinfoInstalled' => $this->hasFileinfoInstalled(),
698
+                'hasWorkingFileLocking' => $this->hasWorkingFileLocking(),
699
+                'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(),
700
+                'cronInfo' => $this->getLastCronInfo(),
701
+                'cronErrors' => $this->getCronErrors(),
702
+                'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(),
703
+                'isMemcacheConfigured' => $this->isMemcacheConfigured(),
704
+                'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'),
705
+                'isRandomnessSecure' => $this->isRandomnessSecure(),
706
+                'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'),
707
+                'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(),
708
+                'phpSupported' => $this->isPhpSupported(),
709
+                'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(),
710
+                'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'),
711
+                'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(),
712
+                'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(),
713
+                'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'),
714
+                'isOpcacheProperlySetup' => $this->isOpcacheProperlySetup(),
715
+                'hasOpcacheLoaded' => $this->hasOpcacheLoaded(),
716
+                'phpOpcacheDocumentation' => $this->urlGenerator->linkToDocs('admin-php-opcache'),
717
+                'isSettimelimitAvailable' => $this->isSettimelimitAvailable(),
718
+                'hasFreeTypeSupport' => $this->hasFreeTypeSupport(),
719
+                'missingIndexes' => $this->hasMissingIndexes(),
720
+                'missingColumns' => $this->hasMissingColumns(),
721
+                'isSqliteUsed' => $this->isSqliteUsed(),
722
+                'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'),
723
+                'isPHPMailerUsed' => $this->isPHPMailerUsed(),
724
+                'mailSettingsDocumentation' => $this->urlGenerator->getAbsoluteURL('index.php/settings/admin'),
725
+                'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(),
726
+                'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(),
727
+                'recommendedPHPModules' => $this->hasRecommendedPHPModules(),
728
+                'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(),
729
+                'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(),
730
+                'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(),
731
+                'reverseProxyGeneratedURL' => $this->urlGenerator->getAbsoluteURL('index.php'),
732
+                PhpDefaultCharset::class => ['pass' => $phpDefaultCharset->run(), 'description' => $phpDefaultCharset->description(), 'severity' => $phpDefaultCharset->severity()],
733
+                PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()],
734
+                LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()],
735
+            ]
736
+        );
737
+    }
738 738
 }
Please login to merge, or discard this patch.
apps/settings/lib/SetupChecks/LegacySSEKeyFormat.php 1 patch
Indentation   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -32,32 +32,32 @@
 block discarded – undo
32 32
 use OCP\IURLGenerator;
33 33
 
34 34
 class LegacySSEKeyFormat {
35
-	/** @var IL10N */
36
-	private $l10n;
37
-	/** @var IConfig */
38
-	private $config;
39
-	/** @var IURLGenerator */
40
-	private $urlGenerator;
41
-
42
-	public function __construct(IL10N $l10n, IConfig $config, IURLGenerator $urlGenerator) {
43
-		$this->l10n = $l10n;
44
-		$this->config = $config;
45
-		$this->urlGenerator = $urlGenerator;
46
-	}
47
-
48
-	public function description(): string {
49
-		return $this->l10n->t('The old server-side-encryption format is enabled. We recommend disabling this.');
50
-	}
51
-
52
-	public function severity(): string {
53
-		return 'warning';
54
-	}
55
-
56
-	public function run(): bool {
57
-		return $this->config->getSystemValueBool('encryption.legacy_format_support', false) === false;
58
-	}
59
-
60
-	public function linkToDocumentation(): string {
61
-		return $this->urlGenerator->linkToDocs('admin-sse-legacy-format');
62
-	}
35
+    /** @var IL10N */
36
+    private $l10n;
37
+    /** @var IConfig */
38
+    private $config;
39
+    /** @var IURLGenerator */
40
+    private $urlGenerator;
41
+
42
+    public function __construct(IL10N $l10n, IConfig $config, IURLGenerator $urlGenerator) {
43
+        $this->l10n = $l10n;
44
+        $this->config = $config;
45
+        $this->urlGenerator = $urlGenerator;
46
+    }
47
+
48
+    public function description(): string {
49
+        return $this->l10n->t('The old server-side-encryption format is enabled. We recommend disabling this.');
50
+    }
51
+
52
+    public function severity(): string {
53
+        return 'warning';
54
+    }
55
+
56
+    public function run(): bool {
57
+        return $this->config->getSystemValueBool('encryption.legacy_format_support', false) === false;
58
+    }
59
+
60
+    public function linkToDocumentation(): string {
61
+        return $this->urlGenerator->linkToDocs('admin-sse-legacy-format');
62
+    }
63 63
 }
Please login to merge, or discard this patch.
lib/private/Repair.php 1 patch
Indentation   +173 added lines, -173 removed lines patch added patch discarded remove patch
@@ -68,177 +68,177 @@
 block discarded – undo
68 68
 
69 69
 class Repair implements IOutput {
70 70
 
71
-	/** @var IRepairStep[] */
72
-	private $repairSteps;
73
-
74
-	/** @var EventDispatcherInterface */
75
-	private $dispatcher;
76
-
77
-	/** @var string */
78
-	private $currentStep;
79
-
80
-	/**
81
-	 * Creates a new repair step runner
82
-	 *
83
-	 * @param IRepairStep[] $repairSteps array of RepairStep instances
84
-	 * @param EventDispatcherInterface $dispatcher
85
-	 */
86
-	public function __construct(array $repairSteps, EventDispatcherInterface $dispatcher) {
87
-		$this->repairSteps = $repairSteps;
88
-		$this->dispatcher  = $dispatcher;
89
-	}
90
-
91
-	/**
92
-	 * Run a series of repair steps for common problems
93
-	 */
94
-	public function run() {
95
-		if (count($this->repairSteps) === 0) {
96
-			$this->emit('\OC\Repair', 'info', ['No repair steps available']);
97
-
98
-			return;
99
-		}
100
-		// run each repair step
101
-		foreach ($this->repairSteps as $step) {
102
-			$this->currentStep = $step->getName();
103
-			$this->emit('\OC\Repair', 'step', [$this->currentStep]);
104
-			$step->run($this);
105
-		}
106
-	}
107
-
108
-	/**
109
-	 * Add repair step
110
-	 *
111
-	 * @param IRepairStep|string $repairStep repair step
112
-	 * @throws \Exception
113
-	 */
114
-	public function addStep($repairStep) {
115
-		if (is_string($repairStep)) {
116
-			try {
117
-				$s = \OC::$server->query($repairStep);
118
-			} catch (QueryException $e) {
119
-				if (class_exists($repairStep)) {
120
-					$s = new $repairStep();
121
-				} else {
122
-					throw new \Exception("Repair step '$repairStep' is unknown");
123
-				}
124
-			}
125
-
126
-			if ($s instanceof IRepairStep) {
127
-				$this->repairSteps[] = $s;
128
-			} else {
129
-				throw new \Exception("Repair step '$repairStep' is not of type \\OCP\\Migration\\IRepairStep");
130
-			}
131
-		} else {
132
-			$this->repairSteps[] = $repairStep;
133
-		}
134
-	}
135
-
136
-	/**
137
-	 * Returns the default repair steps to be run on the
138
-	 * command line or after an upgrade.
139
-	 *
140
-	 * @return IRepairStep[]
141
-	 */
142
-	public static function getRepairSteps() {
143
-		return [
144
-			new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false),
145
-			new RepairMimeTypes(\OC::$server->getConfig()),
146
-			new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()),
147
-			new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
148
-			new MoveUpdaterStepFile(\OC::$server->getConfig()),
149
-			new FixMountStorages(\OC::$server->getDatabaseConnection()),
150
-			new AddLogRotateJob(\OC::$server->getJobList()),
151
-			new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OC::$server->query(SCSSCacher::class), \OC::$server->query(JSCombiner::class)),
152
-			new ClearGeneratedAvatarCache(\OC::$server->getConfig(), \OC::$server->query(AvatarManager::class)),
153
-			new AddPreviewBackgroundCleanupJob(\OC::$server->getJobList()),
154
-			new AddCleanupUpdaterBackupsJob(\OC::$server->getJobList()),
155
-			new CleanupCardDAVPhotoCache(\OC::$server->getConfig(), \OC::$server->getAppDataDir('dav-photocache'), \OC::$server->getLogger()),
156
-			new AddClenupLoginFlowV2BackgroundJob(\OC::$server->getJobList()),
157
-			new RemoveLinkShares(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig(), \OC::$server->getGroupManager(), \OC::$server->getNotificationManager(), \OC::$server->query(ITimeFactory::class)),
158
-			new ClearCollectionsAccessCache(\OC::$server->getConfig(), \OC::$server->query(IManager::class)),
159
-			\OC::$server->query(ResetGeneratedAvatarFlag::class),
160
-			\OC::$server->query(EncryptionLegacyCipher::class),
161
-		];
162
-	}
163
-
164
-	/**
165
-	 * Returns expensive repair steps to be run on the
166
-	 * command line with a special option.
167
-	 *
168
-	 * @return IRepairStep[]
169
-	 */
170
-	public static function getExpensiveRepairSteps() {
171
-		return [
172
-			new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager())
173
-		];
174
-	}
175
-
176
-	/**
177
-	 * Returns the repair steps to be run before an
178
-	 * upgrade.
179
-	 *
180
-	 * @return IRepairStep[]
181
-	 */
182
-	public static function getBeforeUpgradeRepairSteps() {
183
-		$connection = \OC::$server->getDatabaseConnection();
184
-		$config     = \OC::$server->getConfig();
185
-		$steps      = [
186
-			new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true),
187
-			new SqliteAutoincrement($connection),
188
-			new SaveAccountsTableData($connection, $config),
189
-			new DropAccountTermsTable($connection)
190
-		];
191
-
192
-		return $steps;
193
-	}
194
-
195
-	/**
196
-	 * @param string $scope
197
-	 * @param string $method
198
-	 * @param array $arguments
199
-	 */
200
-	public function emit($scope, $method, array $arguments = []) {
201
-		if (!is_null($this->dispatcher)) {
202
-			$this->dispatcher->dispatch("$scope::$method",
203
-				new GenericEvent("$scope::$method", $arguments));
204
-		}
205
-	}
206
-
207
-	public function info($string) {
208
-		// for now just emit as we did in the past
209
-		$this->emit('\OC\Repair', 'info', [$string]);
210
-	}
211
-
212
-	/**
213
-	 * @param string $message
214
-	 */
215
-	public function warning($message) {
216
-		// for now just emit as we did in the past
217
-		$this->emit('\OC\Repair', 'warning', [$message]);
218
-	}
219
-
220
-	/**
221
-	 * @param int $max
222
-	 */
223
-	public function startProgress($max = 0) {
224
-		// for now just emit as we did in the past
225
-		$this->emit('\OC\Repair', 'startProgress', [$max, $this->currentStep]);
226
-	}
227
-
228
-	/**
229
-	 * @param int $step
230
-	 * @param string $description
231
-	 */
232
-	public function advance($step = 1, $description = '') {
233
-		// for now just emit as we did in the past
234
-		$this->emit('\OC\Repair', 'advance', [$step, $description]);
235
-	}
236
-
237
-	/**
238
-	 * @param int $max
239
-	 */
240
-	public function finishProgress() {
241
-		// for now just emit as we did in the past
242
-		$this->emit('\OC\Repair', 'finishProgress', []);
243
-	}
71
+    /** @var IRepairStep[] */
72
+    private $repairSteps;
73
+
74
+    /** @var EventDispatcherInterface */
75
+    private $dispatcher;
76
+
77
+    /** @var string */
78
+    private $currentStep;
79
+
80
+    /**
81
+     * Creates a new repair step runner
82
+     *
83
+     * @param IRepairStep[] $repairSteps array of RepairStep instances
84
+     * @param EventDispatcherInterface $dispatcher
85
+     */
86
+    public function __construct(array $repairSteps, EventDispatcherInterface $dispatcher) {
87
+        $this->repairSteps = $repairSteps;
88
+        $this->dispatcher  = $dispatcher;
89
+    }
90
+
91
+    /**
92
+     * Run a series of repair steps for common problems
93
+     */
94
+    public function run() {
95
+        if (count($this->repairSteps) === 0) {
96
+            $this->emit('\OC\Repair', 'info', ['No repair steps available']);
97
+
98
+            return;
99
+        }
100
+        // run each repair step
101
+        foreach ($this->repairSteps as $step) {
102
+            $this->currentStep = $step->getName();
103
+            $this->emit('\OC\Repair', 'step', [$this->currentStep]);
104
+            $step->run($this);
105
+        }
106
+    }
107
+
108
+    /**
109
+     * Add repair step
110
+     *
111
+     * @param IRepairStep|string $repairStep repair step
112
+     * @throws \Exception
113
+     */
114
+    public function addStep($repairStep) {
115
+        if (is_string($repairStep)) {
116
+            try {
117
+                $s = \OC::$server->query($repairStep);
118
+            } catch (QueryException $e) {
119
+                if (class_exists($repairStep)) {
120
+                    $s = new $repairStep();
121
+                } else {
122
+                    throw new \Exception("Repair step '$repairStep' is unknown");
123
+                }
124
+            }
125
+
126
+            if ($s instanceof IRepairStep) {
127
+                $this->repairSteps[] = $s;
128
+            } else {
129
+                throw new \Exception("Repair step '$repairStep' is not of type \\OCP\\Migration\\IRepairStep");
130
+            }
131
+        } else {
132
+            $this->repairSteps[] = $repairStep;
133
+        }
134
+    }
135
+
136
+    /**
137
+     * Returns the default repair steps to be run on the
138
+     * command line or after an upgrade.
139
+     *
140
+     * @return IRepairStep[]
141
+     */
142
+    public static function getRepairSteps() {
143
+        return [
144
+            new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false),
145
+            new RepairMimeTypes(\OC::$server->getConfig()),
146
+            new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()),
147
+            new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
148
+            new MoveUpdaterStepFile(\OC::$server->getConfig()),
149
+            new FixMountStorages(\OC::$server->getDatabaseConnection()),
150
+            new AddLogRotateJob(\OC::$server->getJobList()),
151
+            new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OC::$server->query(SCSSCacher::class), \OC::$server->query(JSCombiner::class)),
152
+            new ClearGeneratedAvatarCache(\OC::$server->getConfig(), \OC::$server->query(AvatarManager::class)),
153
+            new AddPreviewBackgroundCleanupJob(\OC::$server->getJobList()),
154
+            new AddCleanupUpdaterBackupsJob(\OC::$server->getJobList()),
155
+            new CleanupCardDAVPhotoCache(\OC::$server->getConfig(), \OC::$server->getAppDataDir('dav-photocache'), \OC::$server->getLogger()),
156
+            new AddClenupLoginFlowV2BackgroundJob(\OC::$server->getJobList()),
157
+            new RemoveLinkShares(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig(), \OC::$server->getGroupManager(), \OC::$server->getNotificationManager(), \OC::$server->query(ITimeFactory::class)),
158
+            new ClearCollectionsAccessCache(\OC::$server->getConfig(), \OC::$server->query(IManager::class)),
159
+            \OC::$server->query(ResetGeneratedAvatarFlag::class),
160
+            \OC::$server->query(EncryptionLegacyCipher::class),
161
+        ];
162
+    }
163
+
164
+    /**
165
+     * Returns expensive repair steps to be run on the
166
+     * command line with a special option.
167
+     *
168
+     * @return IRepairStep[]
169
+     */
170
+    public static function getExpensiveRepairSteps() {
171
+        return [
172
+            new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager())
173
+        ];
174
+    }
175
+
176
+    /**
177
+     * Returns the repair steps to be run before an
178
+     * upgrade.
179
+     *
180
+     * @return IRepairStep[]
181
+     */
182
+    public static function getBeforeUpgradeRepairSteps() {
183
+        $connection = \OC::$server->getDatabaseConnection();
184
+        $config     = \OC::$server->getConfig();
185
+        $steps      = [
186
+            new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true),
187
+            new SqliteAutoincrement($connection),
188
+            new SaveAccountsTableData($connection, $config),
189
+            new DropAccountTermsTable($connection)
190
+        ];
191
+
192
+        return $steps;
193
+    }
194
+
195
+    /**
196
+     * @param string $scope
197
+     * @param string $method
198
+     * @param array $arguments
199
+     */
200
+    public function emit($scope, $method, array $arguments = []) {
201
+        if (!is_null($this->dispatcher)) {
202
+            $this->dispatcher->dispatch("$scope::$method",
203
+                new GenericEvent("$scope::$method", $arguments));
204
+        }
205
+    }
206
+
207
+    public function info($string) {
208
+        // for now just emit as we did in the past
209
+        $this->emit('\OC\Repair', 'info', [$string]);
210
+    }
211
+
212
+    /**
213
+     * @param string $message
214
+     */
215
+    public function warning($message) {
216
+        // for now just emit as we did in the past
217
+        $this->emit('\OC\Repair', 'warning', [$message]);
218
+    }
219
+
220
+    /**
221
+     * @param int $max
222
+     */
223
+    public function startProgress($max = 0) {
224
+        // for now just emit as we did in the past
225
+        $this->emit('\OC\Repair', 'startProgress', [$max, $this->currentStep]);
226
+    }
227
+
228
+    /**
229
+     * @param int $step
230
+     * @param string $description
231
+     */
232
+    public function advance($step = 1, $description = '') {
233
+        // for now just emit as we did in the past
234
+        $this->emit('\OC\Repair', 'advance', [$step, $description]);
235
+    }
236
+
237
+    /**
238
+     * @param int $max
239
+     */
240
+    public function finishProgress() {
241
+        // for now just emit as we did in the past
242
+        $this->emit('\OC\Repair', 'finishProgress', []);
243
+    }
244 244
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Wrapper/Encryption.php 1 patch
Indentation   +988 added lines, -988 removed lines patch added patch discarded remove patch
@@ -51,992 +51,992 @@
 block discarded – undo
51 51
 use OCP\ILogger;
52 52
 
53 53
 class Encryption extends Wrapper {
54
-	use LocalTempFileTrait;
55
-
56
-	/** @var string */
57
-	private $mountPoint;
58
-
59
-	/** @var \OC\Encryption\Util */
60
-	private $util;
61
-
62
-	/** @var \OCP\Encryption\IManager */
63
-	private $encryptionManager;
64
-
65
-	/** @var \OCP\ILogger */
66
-	private $logger;
67
-
68
-	/** @var string */
69
-	private $uid;
70
-
71
-	/** @var array */
72
-	protected $unencryptedSize;
73
-
74
-	/** @var \OCP\Encryption\IFile */
75
-	private $fileHelper;
76
-
77
-	/** @var IMountPoint */
78
-	private $mount;
79
-
80
-	/** @var IStorage */
81
-	private $keyStorage;
82
-
83
-	/** @var Update */
84
-	private $update;
85
-
86
-	/** @var Manager */
87
-	private $mountManager;
88
-
89
-	/** @var array remember for which path we execute the repair step to avoid recursions */
90
-	private $fixUnencryptedSizeOf = [];
91
-
92
-	/** @var  ArrayCache */
93
-	private $arrayCache;
94
-
95
-	/**
96
-	 * @param array $parameters
97
-	 * @param IManager $encryptionManager
98
-	 * @param Util $util
99
-	 * @param ILogger $logger
100
-	 * @param IFile $fileHelper
101
-	 * @param string $uid
102
-	 * @param IStorage $keyStorage
103
-	 * @param Update $update
104
-	 * @param Manager $mountManager
105
-	 * @param ArrayCache $arrayCache
106
-	 */
107
-	public function __construct(
108
-		$parameters,
109
-		IManager $encryptionManager = null,
110
-		Util $util = null,
111
-		ILogger $logger = null,
112
-		IFile $fileHelper = null,
113
-		$uid = null,
114
-		IStorage $keyStorage = null,
115
-		Update $update = null,
116
-		Manager $mountManager = null,
117
-		ArrayCache $arrayCache = null
118
-	) {
119
-		$this->mountPoint = $parameters['mountPoint'];
120
-		$this->mount = $parameters['mount'];
121
-		$this->encryptionManager = $encryptionManager;
122
-		$this->util = $util;
123
-		$this->logger = $logger;
124
-		$this->uid = $uid;
125
-		$this->fileHelper = $fileHelper;
126
-		$this->keyStorage = $keyStorage;
127
-		$this->unencryptedSize = [];
128
-		$this->update = $update;
129
-		$this->mountManager = $mountManager;
130
-		$this->arrayCache = $arrayCache;
131
-		parent::__construct($parameters);
132
-	}
133
-
134
-	/**
135
-	 * see http://php.net/manual/en/function.filesize.php
136
-	 * The result for filesize when called on a folder is required to be 0
137
-	 *
138
-	 * @param string $path
139
-	 * @return int
140
-	 */
141
-	public function filesize($path) {
142
-		$fullPath = $this->getFullPath($path);
143
-
144
-		/** @var CacheEntry $info */
145
-		$info = $this->getCache()->get($path);
146
-		if (isset($this->unencryptedSize[$fullPath])) {
147
-			$size = $this->unencryptedSize[$fullPath];
148
-			// update file cache
149
-			if ($info instanceof ICacheEntry) {
150
-				$info = $info->getData();
151
-				$info['encrypted'] = $info['encryptedVersion'];
152
-			} else {
153
-				if (!is_array($info)) {
154
-					$info = [];
155
-				}
156
-				$info['encrypted'] = true;
157
-			}
158
-
159
-			$info['size'] = $size;
160
-			$this->getCache()->put($path, $info);
161
-
162
-			return $size;
163
-		}
164
-
165
-		if (isset($info['fileid']) && $info['encrypted']) {
166
-			return $this->verifyUnencryptedSize($path, $info['size']);
167
-		}
168
-
169
-		return $this->storage->filesize($path);
170
-	}
171
-
172
-	private function modifyMetaData(string $path, array $data): array {
173
-		$fullPath = $this->getFullPath($path);
174
-		$info = $this->getCache()->get($path);
175
-
176
-		if (isset($this->unencryptedSize[$fullPath])) {
177
-			$data['encrypted'] = true;
178
-			$data['size'] = $this->unencryptedSize[$fullPath];
179
-		} else {
180
-			if (isset($info['fileid']) && $info['encrypted']) {
181
-				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
182
-				$data['encrypted'] = true;
183
-			}
184
-		}
185
-
186
-		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
187
-			$data['encryptedVersion'] = $info['encryptedVersion'];
188
-		}
189
-
190
-		return $data;
191
-	}
192
-
193
-	/**
194
-	 * @param string $path
195
-	 * @return array
196
-	 */
197
-	public function getMetaData($path) {
198
-		$data = $this->storage->getMetaData($path);
199
-		if (is_null($data)) {
200
-			return null;
201
-		}
202
-		return $this->modifyMetaData($path, $data);
203
-	}
204
-
205
-	public function getDirectoryContent($directory): \Traversable {
206
-		$parent = rtrim($directory, '/');
207
-		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
208
-			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
209
-		}
210
-	}
211
-
212
-	/**
213
-	 * see http://php.net/manual/en/function.file_get_contents.php
214
-	 *
215
-	 * @param string $path
216
-	 * @return string
217
-	 */
218
-	public function file_get_contents($path) {
219
-		$encryptionModule = $this->getEncryptionModule($path);
220
-
221
-		if ($encryptionModule) {
222
-			$handle = $this->fopen($path, "r");
223
-			if (!$handle) {
224
-				return false;
225
-			}
226
-			$data = stream_get_contents($handle);
227
-			fclose($handle);
228
-			return $data;
229
-		}
230
-		return $this->storage->file_get_contents($path);
231
-	}
232
-
233
-	/**
234
-	 * see http://php.net/manual/en/function.file_put_contents.php
235
-	 *
236
-	 * @param string $path
237
-	 * @param string $data
238
-	 * @return bool
239
-	 */
240
-	public function file_put_contents($path, $data) {
241
-		// file put content will always be translated to a stream write
242
-		$handle = $this->fopen($path, 'w');
243
-		if (is_resource($handle)) {
244
-			$written = fwrite($handle, $data);
245
-			fclose($handle);
246
-			return $written;
247
-		}
248
-
249
-		return false;
250
-	}
251
-
252
-	/**
253
-	 * see http://php.net/manual/en/function.unlink.php
254
-	 *
255
-	 * @param string $path
256
-	 * @return bool
257
-	 */
258
-	public function unlink($path) {
259
-		$fullPath = $this->getFullPath($path);
260
-		if ($this->util->isExcluded($fullPath)) {
261
-			return $this->storage->unlink($path);
262
-		}
263
-
264
-		$encryptionModule = $this->getEncryptionModule($path);
265
-		if ($encryptionModule) {
266
-			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
267
-		}
268
-
269
-		return $this->storage->unlink($path);
270
-	}
271
-
272
-	/**
273
-	 * see http://php.net/manual/en/function.rename.php
274
-	 *
275
-	 * @param string $path1
276
-	 * @param string $path2
277
-	 * @return bool
278
-	 */
279
-	public function rename($path1, $path2) {
280
-		$result = $this->storage->rename($path1, $path2);
281
-
282
-		if ($result &&
283
-			// versions always use the keys from the original file, so we can skip
284
-			// this step for versions
285
-			$this->isVersion($path2) === false &&
286
-			$this->encryptionManager->isEnabled()) {
287
-			$source = $this->getFullPath($path1);
288
-			if (!$this->util->isExcluded($source)) {
289
-				$target = $this->getFullPath($path2);
290
-				if (isset($this->unencryptedSize[$source])) {
291
-					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
292
-				}
293
-				$this->keyStorage->renameKeys($source, $target);
294
-				$module = $this->getEncryptionModule($path2);
295
-				if ($module) {
296
-					$module->update($target, $this->uid, []);
297
-				}
298
-			}
299
-		}
300
-
301
-		return $result;
302
-	}
303
-
304
-	/**
305
-	 * see http://php.net/manual/en/function.rmdir.php
306
-	 *
307
-	 * @param string $path
308
-	 * @return bool
309
-	 */
310
-	public function rmdir($path) {
311
-		$result = $this->storage->rmdir($path);
312
-		$fullPath = $this->getFullPath($path);
313
-		if ($result &&
314
-			$this->util->isExcluded($fullPath) === false &&
315
-			$this->encryptionManager->isEnabled()
316
-		) {
317
-			$this->keyStorage->deleteAllFileKeys($fullPath);
318
-		}
319
-
320
-		return $result;
321
-	}
322
-
323
-	/**
324
-	 * check if a file can be read
325
-	 *
326
-	 * @param string $path
327
-	 * @return bool
328
-	 */
329
-	public function isReadable($path) {
330
-		$isReadable = true;
331
-
332
-		$metaData = $this->getMetaData($path);
333
-		if (
334
-			!$this->is_dir($path) &&
335
-			isset($metaData['encrypted']) &&
336
-			$metaData['encrypted'] === true
337
-		) {
338
-			$fullPath = $this->getFullPath($path);
339
-			$module = $this->getEncryptionModule($path);
340
-			$isReadable = $module->isReadable($fullPath, $this->uid);
341
-		}
342
-
343
-		return $this->storage->isReadable($path) && $isReadable;
344
-	}
345
-
346
-	/**
347
-	 * see http://php.net/manual/en/function.copy.php
348
-	 *
349
-	 * @param string $path1
350
-	 * @param string $path2
351
-	 * @return bool
352
-	 */
353
-	public function copy($path1, $path2) {
354
-		$source = $this->getFullPath($path1);
355
-
356
-		if ($this->util->isExcluded($source)) {
357
-			return $this->storage->copy($path1, $path2);
358
-		}
359
-
360
-		// need to stream copy file by file in case we copy between a encrypted
361
-		// and a unencrypted storage
362
-		$this->unlink($path2);
363
-		return $this->copyFromStorage($this, $path1, $path2);
364
-	}
365
-
366
-	/**
367
-	 * see http://php.net/manual/en/function.fopen.php
368
-	 *
369
-	 * @param string $path
370
-	 * @param string $mode
371
-	 * @return resource|bool
372
-	 * @throws GenericEncryptionException
373
-	 * @throws ModuleDoesNotExistsException
374
-	 */
375
-	public function fopen($path, $mode) {
376
-
377
-		// check if the file is stored in the array cache, this means that we
378
-		// copy a file over to the versions folder, in this case we don't want to
379
-		// decrypt it
380
-		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
381
-			$this->arrayCache->remove('encryption_copy_version_' . $path);
382
-			return $this->storage->fopen($path, $mode);
383
-		}
384
-
385
-		$encryptionEnabled = $this->encryptionManager->isEnabled();
386
-		$shouldEncrypt = false;
387
-		$encryptionModule = null;
388
-		$header = $this->getHeader($path);
389
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
390
-		$fullPath = $this->getFullPath($path);
391
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
392
-
393
-		if ($this->util->isExcluded($fullPath) === false) {
394
-			$size = $unencryptedSize = 0;
395
-			$realFile = $this->util->stripPartialFileExtension($path);
396
-			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
397
-			$targetIsEncrypted = false;
398
-			if ($targetExists) {
399
-				// in case the file exists we require the explicit module as
400
-				// specified in the file header - otherwise we need to fail hard to
401
-				// prevent data loss on client side
402
-				if (!empty($encryptionModuleId)) {
403
-					$targetIsEncrypted = true;
404
-					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
405
-				}
406
-
407
-				if ($this->file_exists($path)) {
408
-					$size = $this->storage->filesize($path);
409
-					$unencryptedSize = $this->filesize($path);
410
-				} else {
411
-					$size = $unencryptedSize = 0;
412
-				}
413
-			}
414
-
415
-			try {
416
-				if (
417
-					$mode === 'w'
418
-					|| $mode === 'w+'
419
-					|| $mode === 'wb'
420
-					|| $mode === 'wb+'
421
-				) {
422
-					// if we update a encrypted file with a un-encrypted one we change the db flag
423
-					if ($targetIsEncrypted && $encryptionEnabled === false) {
424
-						$cache = $this->storage->getCache();
425
-						if ($cache) {
426
-							$entry = $cache->get($path);
427
-							$cache->update($entry->getId(), ['encrypted' => 0]);
428
-						}
429
-					}
430
-					if ($encryptionEnabled) {
431
-						// if $encryptionModuleId is empty, the default module will be used
432
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
433
-						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
434
-						$signed = true;
435
-					}
436
-				} else {
437
-					$info = $this->getCache()->get($path);
438
-					// only get encryption module if we found one in the header
439
-					// or if file should be encrypted according to the file cache
440
-					if (!empty($encryptionModuleId)) {
441
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
442
-						$shouldEncrypt = true;
443
-					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
444
-						// we come from a old installation. No header and/or no module defined
445
-						// but the file is encrypted. In this case we need to use the
446
-						// OC_DEFAULT_MODULE to read the file
447
-						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
448
-						$shouldEncrypt = true;
449
-						$targetIsEncrypted = true;
450
-					}
451
-				}
452
-			} catch (ModuleDoesNotExistsException $e) {
453
-				$this->logger->logException($e, [
454
-					'message' => 'Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted',
455
-					'level' => ILogger::WARN,
456
-					'app' => 'core',
457
-				]);
458
-			}
459
-
460
-			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
461
-			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
462
-				if (!$targetExists || !$targetIsEncrypted) {
463
-					$shouldEncrypt = false;
464
-				}
465
-			}
466
-
467
-			if ($shouldEncrypt === true && $encryptionModule !== null) {
468
-				$headerSize = $this->getHeaderSize($path);
469
-				$source = $this->storage->fopen($path, $mode);
470
-				if (!is_resource($source)) {
471
-					return false;
472
-				}
473
-				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
474
-					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
475
-					$size, $unencryptedSize, $headerSize, $signed);
476
-				return $handle;
477
-			}
478
-		}
479
-
480
-		return $this->storage->fopen($path, $mode);
481
-	}
482
-
483
-
484
-	/**
485
-	 * perform some plausibility checks if the the unencrypted size is correct.
486
-	 * If not, we calculate the correct unencrypted size and return it
487
-	 *
488
-	 * @param string $path internal path relative to the storage root
489
-	 * @param int $unencryptedSize size of the unencrypted file
490
-	 *
491
-	 * @return int unencrypted size
492
-	 */
493
-	protected function verifyUnencryptedSize($path, $unencryptedSize) {
494
-		$size = $this->storage->filesize($path);
495
-		$result = $unencryptedSize;
496
-
497
-		if ($unencryptedSize < 0 ||
498
-			($size > 0 && $unencryptedSize === $size)
499
-		) {
500
-			// check if we already calculate the unencrypted size for the
501
-			// given path to avoid recursions
502
-			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
503
-				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
504
-				try {
505
-					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
506
-				} catch (\Exception $e) {
507
-					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path);
508
-					$this->logger->logException($e);
509
-				}
510
-				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
511
-			}
512
-		}
513
-
514
-		return $result;
515
-	}
516
-
517
-	/**
518
-	 * calculate the unencrypted size
519
-	 *
520
-	 * @param string $path internal path relative to the storage root
521
-	 * @param int $size size of the physical file
522
-	 * @param int $unencryptedSize size of the unencrypted file
523
-	 *
524
-	 * @return int calculated unencrypted size
525
-	 */
526
-	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
527
-		$headerSize = $this->getHeaderSize($path);
528
-		$header = $this->getHeader($path);
529
-		$encryptionModule = $this->getEncryptionModule($path);
530
-
531
-		$stream = $this->storage->fopen($path, 'r');
532
-
533
-		// if we couldn't open the file we return the old unencrypted size
534
-		if (!is_resource($stream)) {
535
-			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
536
-			return $unencryptedSize;
537
-		}
538
-
539
-		$newUnencryptedSize = 0;
540
-		$size -= $headerSize;
541
-		$blockSize = $this->util->getBlockSize();
542
-
543
-		// if a header exists we skip it
544
-		if ($headerSize > 0) {
545
-			fread($stream, $headerSize);
546
-		}
547
-
548
-		// fast path, else the calculation for $lastChunkNr is bogus
549
-		if ($size === 0) {
550
-			return 0;
551
-		}
552
-
553
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
554
-		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
555
-
556
-		// calculate last chunk nr
557
-		// next highest is end of chunks, one subtracted is last one
558
-		// we have to read the last chunk, we can't just calculate it (because of padding etc)
559
-
560
-		$lastChunkNr = ceil($size / $blockSize) - 1;
561
-		// calculate last chunk position
562
-		$lastChunkPos = ($lastChunkNr * $blockSize);
563
-		// try to fseek to the last chunk, if it fails we have to read the whole file
564
-		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
565
-			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
566
-		}
567
-
568
-		$lastChunkContentEncrypted = '';
569
-		$count = $blockSize;
570
-
571
-		while ($count > 0) {
572
-			$data = fread($stream, $blockSize);
573
-			$count = strlen($data);
574
-			$lastChunkContentEncrypted .= $data;
575
-			if (strlen($lastChunkContentEncrypted) > $blockSize) {
576
-				$newUnencryptedSize += $unencryptedBlockSize;
577
-				$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
578
-			}
579
-		}
580
-
581
-		fclose($stream);
582
-
583
-		// we have to decrypt the last chunk to get it actual size
584
-		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
585
-		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
586
-		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
587
-
588
-		// calc the real file size with the size of the last chunk
589
-		$newUnencryptedSize += strlen($decryptedLastChunk);
590
-
591
-		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
592
-
593
-		// write to cache if applicable
594
-		$cache = $this->storage->getCache();
595
-		if ($cache) {
596
-			$entry = $cache->get($path);
597
-			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
598
-		}
599
-
600
-		return $newUnencryptedSize;
601
-	}
602
-
603
-	/**
604
-	 * @param Storage\IStorage $sourceStorage
605
-	 * @param string $sourceInternalPath
606
-	 * @param string $targetInternalPath
607
-	 * @param bool $preserveMtime
608
-	 * @return bool
609
-	 */
610
-	public function moveFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
611
-		if ($sourceStorage === $this) {
612
-			return $this->rename($sourceInternalPath, $targetInternalPath);
613
-		}
614
-
615
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
616
-		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
617
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
618
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
619
-		// - remove $this->copyBetweenStorage
620
-
621
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
622
-			return false;
623
-		}
624
-
625
-		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
626
-		if ($result) {
627
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
628
-				$result &= $sourceStorage->rmdir($sourceInternalPath);
629
-			} else {
630
-				$result &= $sourceStorage->unlink($sourceInternalPath);
631
-			}
632
-		}
633
-		return $result;
634
-	}
635
-
636
-
637
-	/**
638
-	 * @param Storage\IStorage $sourceStorage
639
-	 * @param string $sourceInternalPath
640
-	 * @param string $targetInternalPath
641
-	 * @param bool $preserveMtime
642
-	 * @param bool $isRename
643
-	 * @return bool
644
-	 */
645
-	public function copyFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
646
-
647
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
648
-		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
649
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
650
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
651
-		// - remove $this->copyBetweenStorage
652
-
653
-		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
654
-	}
655
-
656
-	/**
657
-	 * Update the encrypted cache version in the database
658
-	 *
659
-	 * @param Storage\IStorage $sourceStorage
660
-	 * @param string $sourceInternalPath
661
-	 * @param string $targetInternalPath
662
-	 * @param bool $isRename
663
-	 * @param bool $keepEncryptionVersion
664
-	 */
665
-	private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, $keepEncryptionVersion) {
666
-		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
667
-		$cacheInformation = [
668
-			'encrypted' => $isEncrypted,
669
-		];
670
-		if ($isEncrypted) {
671
-			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
672
-
673
-			// In case of a move operation from an unencrypted to an encrypted
674
-			// storage the old encrypted version would stay with "0" while the
675
-			// correct value would be "1". Thus we manually set the value to "1"
676
-			// for those cases.
677
-			// See also https://github.com/owncloud/core/issues/23078
678
-			if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
679
-				$encryptedVersion = 1;
680
-			}
681
-
682
-			$cacheInformation['encryptedVersion'] = $encryptedVersion;
683
-		}
684
-
685
-		// in case of a rename we need to manipulate the source cache because
686
-		// this information will be kept for the new target
687
-		if ($isRename) {
688
-			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
689
-		} else {
690
-			$this->getCache()->put($targetInternalPath, $cacheInformation);
691
-		}
692
-	}
693
-
694
-	/**
695
-	 * copy file between two storages
696
-	 *
697
-	 * @param Storage\IStorage $sourceStorage
698
-	 * @param string $sourceInternalPath
699
-	 * @param string $targetInternalPath
700
-	 * @param bool $preserveMtime
701
-	 * @param bool $isRename
702
-	 * @return bool
703
-	 * @throws \Exception
704
-	 */
705
-	private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
706
-
707
-		// for versions we have nothing to do, because versions should always use the
708
-		// key from the original file. Just create a 1:1 copy and done
709
-		if ($this->isVersion($targetInternalPath) ||
710
-			$this->isVersion($sourceInternalPath)) {
711
-			// remember that we try to create a version so that we can detect it during
712
-			// fopen($sourceInternalPath) and by-pass the encryption in order to
713
-			// create a 1:1 copy of the file
714
-			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
715
-			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
716
-			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
717
-			if ($result) {
718
-				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
719
-				// make sure that we update the unencrypted size for the version
720
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
721
-					$this->updateUnencryptedSize(
722
-						$this->getFullPath($targetInternalPath),
723
-						$info['size']
724
-					);
725
-				}
726
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
727
-			}
728
-			return $result;
729
-		}
730
-
731
-		// first copy the keys that we reuse the existing file key on the target location
732
-		// and don't create a new one which would break versions for example.
733
-		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
734
-		if (count($mount) === 1) {
735
-			$mountPoint = $mount[0]->getMountPoint();
736
-			$source = $mountPoint . '/' . $sourceInternalPath;
737
-			$target = $this->getFullPath($targetInternalPath);
738
-			$this->copyKeys($source, $target);
739
-		} else {
740
-			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
741
-		}
742
-
743
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
744
-			$dh = $sourceStorage->opendir($sourceInternalPath);
745
-			$result = $this->mkdir($targetInternalPath);
746
-			if (is_resource($dh)) {
747
-				while ($result and ($file = readdir($dh)) !== false) {
748
-					if (!Filesystem::isIgnoredDir($file)) {
749
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
750
-					}
751
-				}
752
-			}
753
-		} else {
754
-			try {
755
-				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
756
-				$target = $this->fopen($targetInternalPath, 'w');
757
-				[, $result] = \OC_Helper::streamCopy($source, $target);
758
-				fclose($source);
759
-				fclose($target);
760
-			} catch (\Exception $e) {
761
-				fclose($source);
762
-				fclose($target);
763
-				throw $e;
764
-			}
765
-			if ($result) {
766
-				if ($preserveMtime) {
767
-					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
768
-				}
769
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
770
-			} else {
771
-				// delete partially written target file
772
-				$this->unlink($targetInternalPath);
773
-				// delete cache entry that was created by fopen
774
-				$this->getCache()->remove($targetInternalPath);
775
-			}
776
-		}
777
-		return (bool)$result;
778
-	}
779
-
780
-	/**
781
-	 * get the path to a local version of the file.
782
-	 * The local version of the file can be temporary and doesn't have to be persistent across requests
783
-	 *
784
-	 * @param string $path
785
-	 * @return string
786
-	 */
787
-	public function getLocalFile($path) {
788
-		if ($this->encryptionManager->isEnabled()) {
789
-			$cachedFile = $this->getCachedFile($path);
790
-			if (is_string($cachedFile)) {
791
-				return $cachedFile;
792
-			}
793
-		}
794
-		return $this->storage->getLocalFile($path);
795
-	}
796
-
797
-	/**
798
-	 * Returns the wrapped storage's value for isLocal()
799
-	 *
800
-	 * @return bool wrapped storage's isLocal() value
801
-	 */
802
-	public function isLocal() {
803
-		if ($this->encryptionManager->isEnabled()) {
804
-			return false;
805
-		}
806
-		return $this->storage->isLocal();
807
-	}
808
-
809
-	/**
810
-	 * see http://php.net/manual/en/function.stat.php
811
-	 * only the following keys are required in the result: size and mtime
812
-	 *
813
-	 * @param string $path
814
-	 * @return array
815
-	 */
816
-	public function stat($path) {
817
-		$stat = $this->storage->stat($path);
818
-		$fileSize = $this->filesize($path);
819
-		$stat['size'] = $fileSize;
820
-		$stat[7] = $fileSize;
821
-		$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
822
-		return $stat;
823
-	}
824
-
825
-	/**
826
-	 * see http://php.net/manual/en/function.hash.php
827
-	 *
828
-	 * @param string $type
829
-	 * @param string $path
830
-	 * @param bool $raw
831
-	 * @return string
832
-	 */
833
-	public function hash($type, $path, $raw = false) {
834
-		$fh = $this->fopen($path, 'rb');
835
-		$ctx = hash_init($type);
836
-		hash_update_stream($ctx, $fh);
837
-		fclose($fh);
838
-		return hash_final($ctx, $raw);
839
-	}
840
-
841
-	/**
842
-	 * return full path, including mount point
843
-	 *
844
-	 * @param string $path relative to mount point
845
-	 * @return string full path including mount point
846
-	 */
847
-	protected function getFullPath($path) {
848
-		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
849
-	}
850
-
851
-	/**
852
-	 * read first block of encrypted file, typically this will contain the
853
-	 * encryption header
854
-	 *
855
-	 * @param string $path
856
-	 * @return string
857
-	 */
858
-	protected function readFirstBlock($path) {
859
-		$firstBlock = '';
860
-		if ($this->storage->file_exists($path)) {
861
-			$handle = $this->storage->fopen($path, 'r');
862
-			$firstBlock = fread($handle, $this->util->getHeaderSize());
863
-			fclose($handle);
864
-		}
865
-		return $firstBlock;
866
-	}
867
-
868
-	/**
869
-	 * return header size of given file
870
-	 *
871
-	 * @param string $path
872
-	 * @return int
873
-	 */
874
-	protected function getHeaderSize($path) {
875
-		$headerSize = 0;
876
-		$realFile = $this->util->stripPartialFileExtension($path);
877
-		if ($this->storage->file_exists($realFile)) {
878
-			$path = $realFile;
879
-		}
880
-		$firstBlock = $this->readFirstBlock($path);
881
-
882
-		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
883
-			$headerSize = $this->util->getHeaderSize();
884
-		}
885
-
886
-		return $headerSize;
887
-	}
888
-
889
-	/**
890
-	 * parse raw header to array
891
-	 *
892
-	 * @param string $rawHeader
893
-	 * @return array
894
-	 */
895
-	protected function parseRawHeader($rawHeader) {
896
-		$result = [];
897
-		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
898
-			$header = $rawHeader;
899
-			$endAt = strpos($header, Util::HEADER_END);
900
-			if ($endAt !== false) {
901
-				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
902
-
903
-				// +1 to not start with an ':' which would result in empty element at the beginning
904
-				$exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
905
-
906
-				$element = array_shift($exploded);
907
-				while ($element !== Util::HEADER_END) {
908
-					$result[$element] = array_shift($exploded);
909
-					$element = array_shift($exploded);
910
-				}
911
-			}
912
-		}
913
-
914
-		return $result;
915
-	}
916
-
917
-	/**
918
-	 * read header from file
919
-	 *
920
-	 * @param string $path
921
-	 * @return array
922
-	 */
923
-	protected function getHeader($path) {
924
-		$realFile = $this->util->stripPartialFileExtension($path);
925
-		$exists = $this->storage->file_exists($realFile);
926
-		if ($exists) {
927
-			$path = $realFile;
928
-		}
929
-
930
-		$firstBlock = $this->readFirstBlock($path);
931
-		$result = $this->parseRawHeader($firstBlock);
932
-
933
-		// if the header doesn't contain a encryption module we check if it is a
934
-		// legacy file. If true, we add the default encryption module
935
-		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
936
-			if (!empty($result)) {
937
-				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
938
-			} elseif ($exists) {
939
-				// if the header was empty we have to check first if it is a encrypted file at all
940
-				// We would do query to filecache only if we know that entry in filecache exists
941
-				$info = $this->getCache()->get($path);
942
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
943
-					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
944
-				}
945
-			}
946
-		}
947
-
948
-		return $result;
949
-	}
950
-
951
-	/**
952
-	 * read encryption module needed to read/write the file located at $path
953
-	 *
954
-	 * @param string $path
955
-	 * @return null|\OCP\Encryption\IEncryptionModule
956
-	 * @throws ModuleDoesNotExistsException
957
-	 * @throws \Exception
958
-	 */
959
-	protected function getEncryptionModule($path) {
960
-		$encryptionModule = null;
961
-		$header = $this->getHeader($path);
962
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
963
-		if (!empty($encryptionModuleId)) {
964
-			try {
965
-				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
966
-			} catch (ModuleDoesNotExistsException $e) {
967
-				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
968
-				throw $e;
969
-			}
970
-		}
971
-
972
-		return $encryptionModule;
973
-	}
974
-
975
-	/**
976
-	 * @param string $path
977
-	 * @param int $unencryptedSize
978
-	 */
979
-	public function updateUnencryptedSize($path, $unencryptedSize) {
980
-		$this->unencryptedSize[$path] = $unencryptedSize;
981
-	}
982
-
983
-	/**
984
-	 * copy keys to new location
985
-	 *
986
-	 * @param string $source path relative to data/
987
-	 * @param string $target path relative to data/
988
-	 * @return bool
989
-	 */
990
-	protected function copyKeys($source, $target) {
991
-		if (!$this->util->isExcluded($source)) {
992
-			return $this->keyStorage->copyKeys($source, $target);
993
-		}
994
-
995
-		return false;
996
-	}
997
-
998
-	/**
999
-	 * check if path points to a files version
1000
-	 *
1001
-	 * @param $path
1002
-	 * @return bool
1003
-	 */
1004
-	protected function isVersion($path) {
1005
-		$normalized = Filesystem::normalizePath($path);
1006
-		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1007
-	}
1008
-
1009
-	/**
1010
-	 * check if the given storage should be encrypted or not
1011
-	 *
1012
-	 * @param $path
1013
-	 * @return bool
1014
-	 */
1015
-	protected function shouldEncrypt($path) {
1016
-		$fullPath = $this->getFullPath($path);
1017
-		$mountPointConfig = $this->mount->getOption('encrypt', true);
1018
-		if ($mountPointConfig === false) {
1019
-			return false;
1020
-		}
1021
-
1022
-		try {
1023
-			$encryptionModule = $this->getEncryptionModule($fullPath);
1024
-		} catch (ModuleDoesNotExistsException $e) {
1025
-			return false;
1026
-		}
1027
-
1028
-		if ($encryptionModule === null) {
1029
-			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1030
-		}
1031
-
1032
-		return $encryptionModule->shouldEncrypt($fullPath);
1033
-	}
1034
-
1035
-	public function writeStream(string $path, $stream, int $size = null): int {
1036
-		// always fall back to fopen
1037
-		$target = $this->fopen($path, 'w');
1038
-		[$count, $result] = \OC_Helper::streamCopy($stream, $target);
1039
-		fclose($target);
1040
-		return $count;
1041
-	}
54
+    use LocalTempFileTrait;
55
+
56
+    /** @var string */
57
+    private $mountPoint;
58
+
59
+    /** @var \OC\Encryption\Util */
60
+    private $util;
61
+
62
+    /** @var \OCP\Encryption\IManager */
63
+    private $encryptionManager;
64
+
65
+    /** @var \OCP\ILogger */
66
+    private $logger;
67
+
68
+    /** @var string */
69
+    private $uid;
70
+
71
+    /** @var array */
72
+    protected $unencryptedSize;
73
+
74
+    /** @var \OCP\Encryption\IFile */
75
+    private $fileHelper;
76
+
77
+    /** @var IMountPoint */
78
+    private $mount;
79
+
80
+    /** @var IStorage */
81
+    private $keyStorage;
82
+
83
+    /** @var Update */
84
+    private $update;
85
+
86
+    /** @var Manager */
87
+    private $mountManager;
88
+
89
+    /** @var array remember for which path we execute the repair step to avoid recursions */
90
+    private $fixUnencryptedSizeOf = [];
91
+
92
+    /** @var  ArrayCache */
93
+    private $arrayCache;
94
+
95
+    /**
96
+     * @param array $parameters
97
+     * @param IManager $encryptionManager
98
+     * @param Util $util
99
+     * @param ILogger $logger
100
+     * @param IFile $fileHelper
101
+     * @param string $uid
102
+     * @param IStorage $keyStorage
103
+     * @param Update $update
104
+     * @param Manager $mountManager
105
+     * @param ArrayCache $arrayCache
106
+     */
107
+    public function __construct(
108
+        $parameters,
109
+        IManager $encryptionManager = null,
110
+        Util $util = null,
111
+        ILogger $logger = null,
112
+        IFile $fileHelper = null,
113
+        $uid = null,
114
+        IStorage $keyStorage = null,
115
+        Update $update = null,
116
+        Manager $mountManager = null,
117
+        ArrayCache $arrayCache = null
118
+    ) {
119
+        $this->mountPoint = $parameters['mountPoint'];
120
+        $this->mount = $parameters['mount'];
121
+        $this->encryptionManager = $encryptionManager;
122
+        $this->util = $util;
123
+        $this->logger = $logger;
124
+        $this->uid = $uid;
125
+        $this->fileHelper = $fileHelper;
126
+        $this->keyStorage = $keyStorage;
127
+        $this->unencryptedSize = [];
128
+        $this->update = $update;
129
+        $this->mountManager = $mountManager;
130
+        $this->arrayCache = $arrayCache;
131
+        parent::__construct($parameters);
132
+    }
133
+
134
+    /**
135
+     * see http://php.net/manual/en/function.filesize.php
136
+     * The result for filesize when called on a folder is required to be 0
137
+     *
138
+     * @param string $path
139
+     * @return int
140
+     */
141
+    public function filesize($path) {
142
+        $fullPath = $this->getFullPath($path);
143
+
144
+        /** @var CacheEntry $info */
145
+        $info = $this->getCache()->get($path);
146
+        if (isset($this->unencryptedSize[$fullPath])) {
147
+            $size = $this->unencryptedSize[$fullPath];
148
+            // update file cache
149
+            if ($info instanceof ICacheEntry) {
150
+                $info = $info->getData();
151
+                $info['encrypted'] = $info['encryptedVersion'];
152
+            } else {
153
+                if (!is_array($info)) {
154
+                    $info = [];
155
+                }
156
+                $info['encrypted'] = true;
157
+            }
158
+
159
+            $info['size'] = $size;
160
+            $this->getCache()->put($path, $info);
161
+
162
+            return $size;
163
+        }
164
+
165
+        if (isset($info['fileid']) && $info['encrypted']) {
166
+            return $this->verifyUnencryptedSize($path, $info['size']);
167
+        }
168
+
169
+        return $this->storage->filesize($path);
170
+    }
171
+
172
+    private function modifyMetaData(string $path, array $data): array {
173
+        $fullPath = $this->getFullPath($path);
174
+        $info = $this->getCache()->get($path);
175
+
176
+        if (isset($this->unencryptedSize[$fullPath])) {
177
+            $data['encrypted'] = true;
178
+            $data['size'] = $this->unencryptedSize[$fullPath];
179
+        } else {
180
+            if (isset($info['fileid']) && $info['encrypted']) {
181
+                $data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
182
+                $data['encrypted'] = true;
183
+            }
184
+        }
185
+
186
+        if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
187
+            $data['encryptedVersion'] = $info['encryptedVersion'];
188
+        }
189
+
190
+        return $data;
191
+    }
192
+
193
+    /**
194
+     * @param string $path
195
+     * @return array
196
+     */
197
+    public function getMetaData($path) {
198
+        $data = $this->storage->getMetaData($path);
199
+        if (is_null($data)) {
200
+            return null;
201
+        }
202
+        return $this->modifyMetaData($path, $data);
203
+    }
204
+
205
+    public function getDirectoryContent($directory): \Traversable {
206
+        $parent = rtrim($directory, '/');
207
+        foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
208
+            yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
209
+        }
210
+    }
211
+
212
+    /**
213
+     * see http://php.net/manual/en/function.file_get_contents.php
214
+     *
215
+     * @param string $path
216
+     * @return string
217
+     */
218
+    public function file_get_contents($path) {
219
+        $encryptionModule = $this->getEncryptionModule($path);
220
+
221
+        if ($encryptionModule) {
222
+            $handle = $this->fopen($path, "r");
223
+            if (!$handle) {
224
+                return false;
225
+            }
226
+            $data = stream_get_contents($handle);
227
+            fclose($handle);
228
+            return $data;
229
+        }
230
+        return $this->storage->file_get_contents($path);
231
+    }
232
+
233
+    /**
234
+     * see http://php.net/manual/en/function.file_put_contents.php
235
+     *
236
+     * @param string $path
237
+     * @param string $data
238
+     * @return bool
239
+     */
240
+    public function file_put_contents($path, $data) {
241
+        // file put content will always be translated to a stream write
242
+        $handle = $this->fopen($path, 'w');
243
+        if (is_resource($handle)) {
244
+            $written = fwrite($handle, $data);
245
+            fclose($handle);
246
+            return $written;
247
+        }
248
+
249
+        return false;
250
+    }
251
+
252
+    /**
253
+     * see http://php.net/manual/en/function.unlink.php
254
+     *
255
+     * @param string $path
256
+     * @return bool
257
+     */
258
+    public function unlink($path) {
259
+        $fullPath = $this->getFullPath($path);
260
+        if ($this->util->isExcluded($fullPath)) {
261
+            return $this->storage->unlink($path);
262
+        }
263
+
264
+        $encryptionModule = $this->getEncryptionModule($path);
265
+        if ($encryptionModule) {
266
+            $this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
267
+        }
268
+
269
+        return $this->storage->unlink($path);
270
+    }
271
+
272
+    /**
273
+     * see http://php.net/manual/en/function.rename.php
274
+     *
275
+     * @param string $path1
276
+     * @param string $path2
277
+     * @return bool
278
+     */
279
+    public function rename($path1, $path2) {
280
+        $result = $this->storage->rename($path1, $path2);
281
+
282
+        if ($result &&
283
+            // versions always use the keys from the original file, so we can skip
284
+            // this step for versions
285
+            $this->isVersion($path2) === false &&
286
+            $this->encryptionManager->isEnabled()) {
287
+            $source = $this->getFullPath($path1);
288
+            if (!$this->util->isExcluded($source)) {
289
+                $target = $this->getFullPath($path2);
290
+                if (isset($this->unencryptedSize[$source])) {
291
+                    $this->unencryptedSize[$target] = $this->unencryptedSize[$source];
292
+                }
293
+                $this->keyStorage->renameKeys($source, $target);
294
+                $module = $this->getEncryptionModule($path2);
295
+                if ($module) {
296
+                    $module->update($target, $this->uid, []);
297
+                }
298
+            }
299
+        }
300
+
301
+        return $result;
302
+    }
303
+
304
+    /**
305
+     * see http://php.net/manual/en/function.rmdir.php
306
+     *
307
+     * @param string $path
308
+     * @return bool
309
+     */
310
+    public function rmdir($path) {
311
+        $result = $this->storage->rmdir($path);
312
+        $fullPath = $this->getFullPath($path);
313
+        if ($result &&
314
+            $this->util->isExcluded($fullPath) === false &&
315
+            $this->encryptionManager->isEnabled()
316
+        ) {
317
+            $this->keyStorage->deleteAllFileKeys($fullPath);
318
+        }
319
+
320
+        return $result;
321
+    }
322
+
323
+    /**
324
+     * check if a file can be read
325
+     *
326
+     * @param string $path
327
+     * @return bool
328
+     */
329
+    public function isReadable($path) {
330
+        $isReadable = true;
331
+
332
+        $metaData = $this->getMetaData($path);
333
+        if (
334
+            !$this->is_dir($path) &&
335
+            isset($metaData['encrypted']) &&
336
+            $metaData['encrypted'] === true
337
+        ) {
338
+            $fullPath = $this->getFullPath($path);
339
+            $module = $this->getEncryptionModule($path);
340
+            $isReadable = $module->isReadable($fullPath, $this->uid);
341
+        }
342
+
343
+        return $this->storage->isReadable($path) && $isReadable;
344
+    }
345
+
346
+    /**
347
+     * see http://php.net/manual/en/function.copy.php
348
+     *
349
+     * @param string $path1
350
+     * @param string $path2
351
+     * @return bool
352
+     */
353
+    public function copy($path1, $path2) {
354
+        $source = $this->getFullPath($path1);
355
+
356
+        if ($this->util->isExcluded($source)) {
357
+            return $this->storage->copy($path1, $path2);
358
+        }
359
+
360
+        // need to stream copy file by file in case we copy between a encrypted
361
+        // and a unencrypted storage
362
+        $this->unlink($path2);
363
+        return $this->copyFromStorage($this, $path1, $path2);
364
+    }
365
+
366
+    /**
367
+     * see http://php.net/manual/en/function.fopen.php
368
+     *
369
+     * @param string $path
370
+     * @param string $mode
371
+     * @return resource|bool
372
+     * @throws GenericEncryptionException
373
+     * @throws ModuleDoesNotExistsException
374
+     */
375
+    public function fopen($path, $mode) {
376
+
377
+        // check if the file is stored in the array cache, this means that we
378
+        // copy a file over to the versions folder, in this case we don't want to
379
+        // decrypt it
380
+        if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
381
+            $this->arrayCache->remove('encryption_copy_version_' . $path);
382
+            return $this->storage->fopen($path, $mode);
383
+        }
384
+
385
+        $encryptionEnabled = $this->encryptionManager->isEnabled();
386
+        $shouldEncrypt = false;
387
+        $encryptionModule = null;
388
+        $header = $this->getHeader($path);
389
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
390
+        $fullPath = $this->getFullPath($path);
391
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
392
+
393
+        if ($this->util->isExcluded($fullPath) === false) {
394
+            $size = $unencryptedSize = 0;
395
+            $realFile = $this->util->stripPartialFileExtension($path);
396
+            $targetExists = $this->file_exists($realFile) || $this->file_exists($path);
397
+            $targetIsEncrypted = false;
398
+            if ($targetExists) {
399
+                // in case the file exists we require the explicit module as
400
+                // specified in the file header - otherwise we need to fail hard to
401
+                // prevent data loss on client side
402
+                if (!empty($encryptionModuleId)) {
403
+                    $targetIsEncrypted = true;
404
+                    $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
405
+                }
406
+
407
+                if ($this->file_exists($path)) {
408
+                    $size = $this->storage->filesize($path);
409
+                    $unencryptedSize = $this->filesize($path);
410
+                } else {
411
+                    $size = $unencryptedSize = 0;
412
+                }
413
+            }
414
+
415
+            try {
416
+                if (
417
+                    $mode === 'w'
418
+                    || $mode === 'w+'
419
+                    || $mode === 'wb'
420
+                    || $mode === 'wb+'
421
+                ) {
422
+                    // if we update a encrypted file with a un-encrypted one we change the db flag
423
+                    if ($targetIsEncrypted && $encryptionEnabled === false) {
424
+                        $cache = $this->storage->getCache();
425
+                        if ($cache) {
426
+                            $entry = $cache->get($path);
427
+                            $cache->update($entry->getId(), ['encrypted' => 0]);
428
+                        }
429
+                    }
430
+                    if ($encryptionEnabled) {
431
+                        // if $encryptionModuleId is empty, the default module will be used
432
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
433
+                        $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
434
+                        $signed = true;
435
+                    }
436
+                } else {
437
+                    $info = $this->getCache()->get($path);
438
+                    // only get encryption module if we found one in the header
439
+                    // or if file should be encrypted according to the file cache
440
+                    if (!empty($encryptionModuleId)) {
441
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
442
+                        $shouldEncrypt = true;
443
+                    } elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
444
+                        // we come from a old installation. No header and/or no module defined
445
+                        // but the file is encrypted. In this case we need to use the
446
+                        // OC_DEFAULT_MODULE to read the file
447
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
448
+                        $shouldEncrypt = true;
449
+                        $targetIsEncrypted = true;
450
+                    }
451
+                }
452
+            } catch (ModuleDoesNotExistsException $e) {
453
+                $this->logger->logException($e, [
454
+                    'message' => 'Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted',
455
+                    'level' => ILogger::WARN,
456
+                    'app' => 'core',
457
+                ]);
458
+            }
459
+
460
+            // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
461
+            if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
462
+                if (!$targetExists || !$targetIsEncrypted) {
463
+                    $shouldEncrypt = false;
464
+                }
465
+            }
466
+
467
+            if ($shouldEncrypt === true && $encryptionModule !== null) {
468
+                $headerSize = $this->getHeaderSize($path);
469
+                $source = $this->storage->fopen($path, $mode);
470
+                if (!is_resource($source)) {
471
+                    return false;
472
+                }
473
+                $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
474
+                    $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
475
+                    $size, $unencryptedSize, $headerSize, $signed);
476
+                return $handle;
477
+            }
478
+        }
479
+
480
+        return $this->storage->fopen($path, $mode);
481
+    }
482
+
483
+
484
+    /**
485
+     * perform some plausibility checks if the the unencrypted size is correct.
486
+     * If not, we calculate the correct unencrypted size and return it
487
+     *
488
+     * @param string $path internal path relative to the storage root
489
+     * @param int $unencryptedSize size of the unencrypted file
490
+     *
491
+     * @return int unencrypted size
492
+     */
493
+    protected function verifyUnencryptedSize($path, $unencryptedSize) {
494
+        $size = $this->storage->filesize($path);
495
+        $result = $unencryptedSize;
496
+
497
+        if ($unencryptedSize < 0 ||
498
+            ($size > 0 && $unencryptedSize === $size)
499
+        ) {
500
+            // check if we already calculate the unencrypted size for the
501
+            // given path to avoid recursions
502
+            if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
503
+                $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
504
+                try {
505
+                    $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
506
+                } catch (\Exception $e) {
507
+                    $this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path);
508
+                    $this->logger->logException($e);
509
+                }
510
+                unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
511
+            }
512
+        }
513
+
514
+        return $result;
515
+    }
516
+
517
+    /**
518
+     * calculate the unencrypted size
519
+     *
520
+     * @param string $path internal path relative to the storage root
521
+     * @param int $size size of the physical file
522
+     * @param int $unencryptedSize size of the unencrypted file
523
+     *
524
+     * @return int calculated unencrypted size
525
+     */
526
+    protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
527
+        $headerSize = $this->getHeaderSize($path);
528
+        $header = $this->getHeader($path);
529
+        $encryptionModule = $this->getEncryptionModule($path);
530
+
531
+        $stream = $this->storage->fopen($path, 'r');
532
+
533
+        // if we couldn't open the file we return the old unencrypted size
534
+        if (!is_resource($stream)) {
535
+            $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
536
+            return $unencryptedSize;
537
+        }
538
+
539
+        $newUnencryptedSize = 0;
540
+        $size -= $headerSize;
541
+        $blockSize = $this->util->getBlockSize();
542
+
543
+        // if a header exists we skip it
544
+        if ($headerSize > 0) {
545
+            fread($stream, $headerSize);
546
+        }
547
+
548
+        // fast path, else the calculation for $lastChunkNr is bogus
549
+        if ($size === 0) {
550
+            return 0;
551
+        }
552
+
553
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
554
+        $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
555
+
556
+        // calculate last chunk nr
557
+        // next highest is end of chunks, one subtracted is last one
558
+        // we have to read the last chunk, we can't just calculate it (because of padding etc)
559
+
560
+        $lastChunkNr = ceil($size / $blockSize) - 1;
561
+        // calculate last chunk position
562
+        $lastChunkPos = ($lastChunkNr * $blockSize);
563
+        // try to fseek to the last chunk, if it fails we have to read the whole file
564
+        if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
565
+            $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
566
+        }
567
+
568
+        $lastChunkContentEncrypted = '';
569
+        $count = $blockSize;
570
+
571
+        while ($count > 0) {
572
+            $data = fread($stream, $blockSize);
573
+            $count = strlen($data);
574
+            $lastChunkContentEncrypted .= $data;
575
+            if (strlen($lastChunkContentEncrypted) > $blockSize) {
576
+                $newUnencryptedSize += $unencryptedBlockSize;
577
+                $lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
578
+            }
579
+        }
580
+
581
+        fclose($stream);
582
+
583
+        // we have to decrypt the last chunk to get it actual size
584
+        $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
585
+        $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
586
+        $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
587
+
588
+        // calc the real file size with the size of the last chunk
589
+        $newUnencryptedSize += strlen($decryptedLastChunk);
590
+
591
+        $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
592
+
593
+        // write to cache if applicable
594
+        $cache = $this->storage->getCache();
595
+        if ($cache) {
596
+            $entry = $cache->get($path);
597
+            $cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
598
+        }
599
+
600
+        return $newUnencryptedSize;
601
+    }
602
+
603
+    /**
604
+     * @param Storage\IStorage $sourceStorage
605
+     * @param string $sourceInternalPath
606
+     * @param string $targetInternalPath
607
+     * @param bool $preserveMtime
608
+     * @return bool
609
+     */
610
+    public function moveFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
611
+        if ($sourceStorage === $this) {
612
+            return $this->rename($sourceInternalPath, $targetInternalPath);
613
+        }
614
+
615
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
616
+        // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
617
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
618
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
619
+        // - remove $this->copyBetweenStorage
620
+
621
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
622
+            return false;
623
+        }
624
+
625
+        $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
626
+        if ($result) {
627
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
628
+                $result &= $sourceStorage->rmdir($sourceInternalPath);
629
+            } else {
630
+                $result &= $sourceStorage->unlink($sourceInternalPath);
631
+            }
632
+        }
633
+        return $result;
634
+    }
635
+
636
+
637
+    /**
638
+     * @param Storage\IStorage $sourceStorage
639
+     * @param string $sourceInternalPath
640
+     * @param string $targetInternalPath
641
+     * @param bool $preserveMtime
642
+     * @param bool $isRename
643
+     * @return bool
644
+     */
645
+    public function copyFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
646
+
647
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
648
+        // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
649
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
650
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
651
+        // - remove $this->copyBetweenStorage
652
+
653
+        return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
654
+    }
655
+
656
+    /**
657
+     * Update the encrypted cache version in the database
658
+     *
659
+     * @param Storage\IStorage $sourceStorage
660
+     * @param string $sourceInternalPath
661
+     * @param string $targetInternalPath
662
+     * @param bool $isRename
663
+     * @param bool $keepEncryptionVersion
664
+     */
665
+    private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, $keepEncryptionVersion) {
666
+        $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
667
+        $cacheInformation = [
668
+            'encrypted' => $isEncrypted,
669
+        ];
670
+        if ($isEncrypted) {
671
+            $encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
672
+
673
+            // In case of a move operation from an unencrypted to an encrypted
674
+            // storage the old encrypted version would stay with "0" while the
675
+            // correct value would be "1". Thus we manually set the value to "1"
676
+            // for those cases.
677
+            // See also https://github.com/owncloud/core/issues/23078
678
+            if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
679
+                $encryptedVersion = 1;
680
+            }
681
+
682
+            $cacheInformation['encryptedVersion'] = $encryptedVersion;
683
+        }
684
+
685
+        // in case of a rename we need to manipulate the source cache because
686
+        // this information will be kept for the new target
687
+        if ($isRename) {
688
+            $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
689
+        } else {
690
+            $this->getCache()->put($targetInternalPath, $cacheInformation);
691
+        }
692
+    }
693
+
694
+    /**
695
+     * copy file between two storages
696
+     *
697
+     * @param Storage\IStorage $sourceStorage
698
+     * @param string $sourceInternalPath
699
+     * @param string $targetInternalPath
700
+     * @param bool $preserveMtime
701
+     * @param bool $isRename
702
+     * @return bool
703
+     * @throws \Exception
704
+     */
705
+    private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
706
+
707
+        // for versions we have nothing to do, because versions should always use the
708
+        // key from the original file. Just create a 1:1 copy and done
709
+        if ($this->isVersion($targetInternalPath) ||
710
+            $this->isVersion($sourceInternalPath)) {
711
+            // remember that we try to create a version so that we can detect it during
712
+            // fopen($sourceInternalPath) and by-pass the encryption in order to
713
+            // create a 1:1 copy of the file
714
+            $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
715
+            $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
716
+            $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
717
+            if ($result) {
718
+                $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
719
+                // make sure that we update the unencrypted size for the version
720
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
721
+                    $this->updateUnencryptedSize(
722
+                        $this->getFullPath($targetInternalPath),
723
+                        $info['size']
724
+                    );
725
+                }
726
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
727
+            }
728
+            return $result;
729
+        }
730
+
731
+        // first copy the keys that we reuse the existing file key on the target location
732
+        // and don't create a new one which would break versions for example.
733
+        $mount = $this->mountManager->findByStorageId($sourceStorage->getId());
734
+        if (count($mount) === 1) {
735
+            $mountPoint = $mount[0]->getMountPoint();
736
+            $source = $mountPoint . '/' . $sourceInternalPath;
737
+            $target = $this->getFullPath($targetInternalPath);
738
+            $this->copyKeys($source, $target);
739
+        } else {
740
+            $this->logger->error('Could not find mount point, can\'t keep encryption keys');
741
+        }
742
+
743
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
744
+            $dh = $sourceStorage->opendir($sourceInternalPath);
745
+            $result = $this->mkdir($targetInternalPath);
746
+            if (is_resource($dh)) {
747
+                while ($result and ($file = readdir($dh)) !== false) {
748
+                    if (!Filesystem::isIgnoredDir($file)) {
749
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
750
+                    }
751
+                }
752
+            }
753
+        } else {
754
+            try {
755
+                $source = $sourceStorage->fopen($sourceInternalPath, 'r');
756
+                $target = $this->fopen($targetInternalPath, 'w');
757
+                [, $result] = \OC_Helper::streamCopy($source, $target);
758
+                fclose($source);
759
+                fclose($target);
760
+            } catch (\Exception $e) {
761
+                fclose($source);
762
+                fclose($target);
763
+                throw $e;
764
+            }
765
+            if ($result) {
766
+                if ($preserveMtime) {
767
+                    $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
768
+                }
769
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
770
+            } else {
771
+                // delete partially written target file
772
+                $this->unlink($targetInternalPath);
773
+                // delete cache entry that was created by fopen
774
+                $this->getCache()->remove($targetInternalPath);
775
+            }
776
+        }
777
+        return (bool)$result;
778
+    }
779
+
780
+    /**
781
+     * get the path to a local version of the file.
782
+     * The local version of the file can be temporary and doesn't have to be persistent across requests
783
+     *
784
+     * @param string $path
785
+     * @return string
786
+     */
787
+    public function getLocalFile($path) {
788
+        if ($this->encryptionManager->isEnabled()) {
789
+            $cachedFile = $this->getCachedFile($path);
790
+            if (is_string($cachedFile)) {
791
+                return $cachedFile;
792
+            }
793
+        }
794
+        return $this->storage->getLocalFile($path);
795
+    }
796
+
797
+    /**
798
+     * Returns the wrapped storage's value for isLocal()
799
+     *
800
+     * @return bool wrapped storage's isLocal() value
801
+     */
802
+    public function isLocal() {
803
+        if ($this->encryptionManager->isEnabled()) {
804
+            return false;
805
+        }
806
+        return $this->storage->isLocal();
807
+    }
808
+
809
+    /**
810
+     * see http://php.net/manual/en/function.stat.php
811
+     * only the following keys are required in the result: size and mtime
812
+     *
813
+     * @param string $path
814
+     * @return array
815
+     */
816
+    public function stat($path) {
817
+        $stat = $this->storage->stat($path);
818
+        $fileSize = $this->filesize($path);
819
+        $stat['size'] = $fileSize;
820
+        $stat[7] = $fileSize;
821
+        $stat['hasHeader'] = $this->getHeaderSize($path) > 0;
822
+        return $stat;
823
+    }
824
+
825
+    /**
826
+     * see http://php.net/manual/en/function.hash.php
827
+     *
828
+     * @param string $type
829
+     * @param string $path
830
+     * @param bool $raw
831
+     * @return string
832
+     */
833
+    public function hash($type, $path, $raw = false) {
834
+        $fh = $this->fopen($path, 'rb');
835
+        $ctx = hash_init($type);
836
+        hash_update_stream($ctx, $fh);
837
+        fclose($fh);
838
+        return hash_final($ctx, $raw);
839
+    }
840
+
841
+    /**
842
+     * return full path, including mount point
843
+     *
844
+     * @param string $path relative to mount point
845
+     * @return string full path including mount point
846
+     */
847
+    protected function getFullPath($path) {
848
+        return Filesystem::normalizePath($this->mountPoint . '/' . $path);
849
+    }
850
+
851
+    /**
852
+     * read first block of encrypted file, typically this will contain the
853
+     * encryption header
854
+     *
855
+     * @param string $path
856
+     * @return string
857
+     */
858
+    protected function readFirstBlock($path) {
859
+        $firstBlock = '';
860
+        if ($this->storage->file_exists($path)) {
861
+            $handle = $this->storage->fopen($path, 'r');
862
+            $firstBlock = fread($handle, $this->util->getHeaderSize());
863
+            fclose($handle);
864
+        }
865
+        return $firstBlock;
866
+    }
867
+
868
+    /**
869
+     * return header size of given file
870
+     *
871
+     * @param string $path
872
+     * @return int
873
+     */
874
+    protected function getHeaderSize($path) {
875
+        $headerSize = 0;
876
+        $realFile = $this->util->stripPartialFileExtension($path);
877
+        if ($this->storage->file_exists($realFile)) {
878
+            $path = $realFile;
879
+        }
880
+        $firstBlock = $this->readFirstBlock($path);
881
+
882
+        if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
883
+            $headerSize = $this->util->getHeaderSize();
884
+        }
885
+
886
+        return $headerSize;
887
+    }
888
+
889
+    /**
890
+     * parse raw header to array
891
+     *
892
+     * @param string $rawHeader
893
+     * @return array
894
+     */
895
+    protected function parseRawHeader($rawHeader) {
896
+        $result = [];
897
+        if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
898
+            $header = $rawHeader;
899
+            $endAt = strpos($header, Util::HEADER_END);
900
+            if ($endAt !== false) {
901
+                $header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
902
+
903
+                // +1 to not start with an ':' which would result in empty element at the beginning
904
+                $exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
905
+
906
+                $element = array_shift($exploded);
907
+                while ($element !== Util::HEADER_END) {
908
+                    $result[$element] = array_shift($exploded);
909
+                    $element = array_shift($exploded);
910
+                }
911
+            }
912
+        }
913
+
914
+        return $result;
915
+    }
916
+
917
+    /**
918
+     * read header from file
919
+     *
920
+     * @param string $path
921
+     * @return array
922
+     */
923
+    protected function getHeader($path) {
924
+        $realFile = $this->util->stripPartialFileExtension($path);
925
+        $exists = $this->storage->file_exists($realFile);
926
+        if ($exists) {
927
+            $path = $realFile;
928
+        }
929
+
930
+        $firstBlock = $this->readFirstBlock($path);
931
+        $result = $this->parseRawHeader($firstBlock);
932
+
933
+        // if the header doesn't contain a encryption module we check if it is a
934
+        // legacy file. If true, we add the default encryption module
935
+        if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
936
+            if (!empty($result)) {
937
+                $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
938
+            } elseif ($exists) {
939
+                // if the header was empty we have to check first if it is a encrypted file at all
940
+                // We would do query to filecache only if we know that entry in filecache exists
941
+                $info = $this->getCache()->get($path);
942
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
943
+                    $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
944
+                }
945
+            }
946
+        }
947
+
948
+        return $result;
949
+    }
950
+
951
+    /**
952
+     * read encryption module needed to read/write the file located at $path
953
+     *
954
+     * @param string $path
955
+     * @return null|\OCP\Encryption\IEncryptionModule
956
+     * @throws ModuleDoesNotExistsException
957
+     * @throws \Exception
958
+     */
959
+    protected function getEncryptionModule($path) {
960
+        $encryptionModule = null;
961
+        $header = $this->getHeader($path);
962
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
963
+        if (!empty($encryptionModuleId)) {
964
+            try {
965
+                $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
966
+            } catch (ModuleDoesNotExistsException $e) {
967
+                $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
968
+                throw $e;
969
+            }
970
+        }
971
+
972
+        return $encryptionModule;
973
+    }
974
+
975
+    /**
976
+     * @param string $path
977
+     * @param int $unencryptedSize
978
+     */
979
+    public function updateUnencryptedSize($path, $unencryptedSize) {
980
+        $this->unencryptedSize[$path] = $unencryptedSize;
981
+    }
982
+
983
+    /**
984
+     * copy keys to new location
985
+     *
986
+     * @param string $source path relative to data/
987
+     * @param string $target path relative to data/
988
+     * @return bool
989
+     */
990
+    protected function copyKeys($source, $target) {
991
+        if (!$this->util->isExcluded($source)) {
992
+            return $this->keyStorage->copyKeys($source, $target);
993
+        }
994
+
995
+        return false;
996
+    }
997
+
998
+    /**
999
+     * check if path points to a files version
1000
+     *
1001
+     * @param $path
1002
+     * @return bool
1003
+     */
1004
+    protected function isVersion($path) {
1005
+        $normalized = Filesystem::normalizePath($path);
1006
+        return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1007
+    }
1008
+
1009
+    /**
1010
+     * check if the given storage should be encrypted or not
1011
+     *
1012
+     * @param $path
1013
+     * @return bool
1014
+     */
1015
+    protected function shouldEncrypt($path) {
1016
+        $fullPath = $this->getFullPath($path);
1017
+        $mountPointConfig = $this->mount->getOption('encrypt', true);
1018
+        if ($mountPointConfig === false) {
1019
+            return false;
1020
+        }
1021
+
1022
+        try {
1023
+            $encryptionModule = $this->getEncryptionModule($fullPath);
1024
+        } catch (ModuleDoesNotExistsException $e) {
1025
+            return false;
1026
+        }
1027
+
1028
+        if ($encryptionModule === null) {
1029
+            $encryptionModule = $this->encryptionManager->getEncryptionModule();
1030
+        }
1031
+
1032
+        return $encryptionModule->shouldEncrypt($fullPath);
1033
+    }
1034
+
1035
+    public function writeStream(string $path, $stream, int $size = null): int {
1036
+        // always fall back to fopen
1037
+        $target = $this->fopen($path, 'w');
1038
+        [$count, $result] = \OC_Helper::streamCopy($stream, $target);
1039
+        fclose($target);
1040
+        return $count;
1041
+    }
1042 1042
 }
Please login to merge, or discard this patch.
lib/private/Repair/NC20/EncryptionLegacyCipher.php 1 patch
Indentation   +27 added lines, -27 removed lines patch added patch discarded remove patch
@@ -32,31 +32,31 @@
 block discarded – undo
32 32
 
33 33
 class EncryptionLegacyCipher implements IRepairStep {
34 34
 
35
-	/** @var IConfig */
36
-	private $config;
37
-	/** @var IManager */
38
-	private $manager;
39
-
40
-	public function __construct(IConfig $config,
41
-								IManager $manager) {
42
-		$this->config = $config;
43
-		$this->manager = $manager;
44
-	}
45
-
46
-	public function getName(): string {
47
-		return 'Keep legacy encryption enabled';
48
-	}
49
-
50
-	private function shouldRun(): bool {
51
-		$versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0');
52
-		return version_compare($versionFromBeforeUpdate, '20.0.0.0', '<=');
53
-	}
54
-
55
-	public function run(IOutput $output): void {
56
-		if ($this->manager->isEnabled()) {
57
-			if ($this->config->getSystemValue('encryption.legacy_format_support', '') === '') {
58
-				$this->config->setSystemValue('encryption.legacy_format_support', true);
59
-			}
60
-		}
61
-	}
35
+    /** @var IConfig */
36
+    private $config;
37
+    /** @var IManager */
38
+    private $manager;
39
+
40
+    public function __construct(IConfig $config,
41
+                                IManager $manager) {
42
+        $this->config = $config;
43
+        $this->manager = $manager;
44
+    }
45
+
46
+    public function getName(): string {
47
+        return 'Keep legacy encryption enabled';
48
+    }
49
+
50
+    private function shouldRun(): bool {
51
+        $versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0.0');
52
+        return version_compare($versionFromBeforeUpdate, '20.0.0.0', '<=');
53
+    }
54
+
55
+    public function run(IOutput $output): void {
56
+        if ($this->manager->isEnabled()) {
57
+            if ($this->config->getSystemValue('encryption.legacy_format_support', '') === '') {
58
+                $this->config->setSystemValue('encryption.legacy_format_support', true);
59
+            }
60
+        }
61
+    }
62 62
 }
Please login to merge, or discard this patch.