Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Util often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Util, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
37 | class Util { |
||
38 | |||
39 | const MIGRATION_COMPLETED = 1; // migration to new encryption completed |
||
40 | const MIGRATION_IN_PROGRESS = -1; // migration is running |
||
41 | const MIGRATION_OPEN = 0; // user still needs to be migrated |
||
42 | |||
43 | const FILE_TYPE_FILE = 0; |
||
44 | const FILE_TYPE_VERSION = 1; |
||
45 | const FILE_TYPE_CACHE = 2; |
||
46 | |||
47 | /** |
||
48 | * @var \OC\Files\View |
||
49 | */ |
||
50 | private $view; // OC\Files\View object for filesystem operations |
||
51 | |||
52 | /** |
||
53 | * @var string |
||
54 | */ |
||
55 | private $userId; // ID of the user we use to encrypt/decrypt files |
||
56 | |||
57 | /** |
||
58 | * @var string |
||
59 | */ |
||
60 | private $keyId; // ID of the key we want to manipulate |
||
61 | |||
62 | /** |
||
63 | * @var bool |
||
64 | */ |
||
65 | private $client; // Client side encryption mode flag |
||
66 | |||
67 | /** |
||
68 | * @var string |
||
69 | */ |
||
70 | private $publicKeyDir; // Dir containing all public user keys |
||
71 | |||
72 | /** |
||
73 | * @var string |
||
74 | */ |
||
75 | private $encryptionDir; // Dir containing user's files_encryption |
||
76 | |||
77 | /** |
||
78 | * @var string |
||
79 | */ |
||
80 | private $keysPath; // Dir containing all file related encryption keys |
||
81 | |||
82 | /** |
||
83 | * @var string |
||
84 | */ |
||
85 | private $publicKeyPath; // Path to user's public key |
||
86 | |||
87 | /** |
||
88 | * @var string |
||
89 | */ |
||
90 | private $privateKeyPath; // Path to user's private key |
||
91 | |||
92 | /** |
||
93 | * @var string |
||
94 | */ |
||
95 | private $userFilesDir; |
||
96 | |||
97 | /** |
||
98 | * @var string |
||
99 | */ |
||
100 | private $publicShareKeyId; |
||
101 | |||
102 | /** |
||
103 | * @var string |
||
104 | */ |
||
105 | private $recoveryKeyId; |
||
106 | |||
107 | /** |
||
108 | * @var bool |
||
109 | */ |
||
110 | private $isPublic; |
||
111 | |||
112 | /** |
||
113 | * @param \OC\Files\View $view |
||
114 | * @param string $userId |
||
115 | * @param bool $client |
||
116 | */ |
||
117 | public function __construct($view, $userId, $client = false) { |
||
118 | |||
119 | $this->view = $view; |
||
120 | $this->client = $client; |
||
121 | $this->userId = $userId; |
||
122 | |||
123 | $appConfig = \OC::$server->getAppConfig(); |
||
124 | |||
125 | $this->publicShareKeyId = $appConfig->getValue('files_encryption', 'publicShareKeyId'); |
||
126 | $this->recoveryKeyId = $appConfig->getValue('files_encryption', 'recoveryKeyId'); |
||
127 | |||
128 | $this->userDir = '/' . $this->userId; |
||
|
|||
129 | $this->fileFolderName = 'files'; |
||
130 | $this->userFilesDir = |
||
131 | '/' . $userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? |
||
132 | $this->publicKeyDir = Keymanager::getPublicKeyPath(); |
||
133 | $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; |
||
134 | $this->keysPath = $this->encryptionDir . '/' . 'keys'; |
||
135 | $this->publicKeyPath = |
||
136 | $this->publicKeyDir . '/' . $this->userId . '.publicKey'; // e.g. data/public-keys/admin.publicKey |
||
137 | $this->privateKeyPath = |
||
138 | $this->encryptionDir . '/' . $this->userId . '.privateKey'; // e.g. data/admin/admin.privateKey |
||
139 | // make sure that the owners home is mounted |
||
140 | \OC\Files\Filesystem::initMountPoints($userId); |
||
141 | |||
142 | if (Helper::isPublicAccess()) { |
||
143 | $this->keyId = $this->publicShareKeyId; |
||
144 | $this->isPublic = true; |
||
145 | } else { |
||
146 | $this->keyId = $this->userId; |
||
147 | $this->isPublic = false; |
||
148 | } |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @return bool |
||
153 | */ |
||
154 | public function ready() { |
||
155 | |||
156 | if ( |
||
157 | !$this->view->file_exists($this->encryptionDir) |
||
158 | or !$this->view->file_exists($this->keysPath) |
||
159 | or !$this->view->file_exists($this->publicKeyPath) |
||
160 | or !$this->view->file_exists($this->privateKeyPath) |
||
161 | ) { |
||
162 | return false; |
||
163 | } else { |
||
164 | return true; |
||
165 | } |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * check if the users private & public key exists |
||
170 | * @return boolean |
||
171 | */ |
||
172 | public function userKeysExists() { |
||
173 | if ( |
||
174 | $this->view->file_exists($this->privateKeyPath) && |
||
175 | $this->view->file_exists($this->publicKeyPath)) { |
||
176 | return true; |
||
177 | } else { |
||
178 | return false; |
||
179 | } |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * create a new public/private key pair for the user |
||
184 | * |
||
185 | * @param string $password password for the private key |
||
186 | */ |
||
187 | public function replaceUserKeys($password) { |
||
188 | $this->backupAllKeys('password_reset'); |
||
189 | $this->view->unlink($this->publicKeyPath); |
||
190 | $this->view->unlink($this->privateKeyPath); |
||
191 | $this->setupServerSide($password); |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * Sets up user folders and keys for serverside encryption |
||
196 | * |
||
197 | * @param string $passphrase to encrypt server-stored private key with |
||
198 | * @return bool |
||
199 | */ |
||
200 | public function setupServerSide($passphrase = null) { |
||
201 | |||
202 | // Set directories to check / create |
||
203 | $setUpDirs = array( |
||
204 | $this->userDir, |
||
205 | $this->publicKeyDir, |
||
206 | $this->encryptionDir, |
||
207 | $this->keysPath |
||
208 | ); |
||
209 | |||
210 | // Check / create all necessary dirs |
||
211 | foreach ($setUpDirs as $dirPath) { |
||
212 | |||
213 | if (!$this->view->file_exists($dirPath)) { |
||
214 | |||
215 | $this->view->mkdir($dirPath); |
||
216 | |||
217 | } |
||
218 | |||
219 | } |
||
220 | |||
221 | // Create user keypair |
||
222 | // we should never override a keyfile |
||
223 | if ( |
||
224 | !$this->view->file_exists($this->publicKeyPath) |
||
225 | && !$this->view->file_exists($this->privateKeyPath) |
||
226 | ) { |
||
227 | |||
228 | // Generate keypair |
||
229 | $keypair = Crypt::createKeypair(); |
||
230 | |||
231 | if ($keypair) { |
||
232 | |||
233 | \OC_FileProxy::$enabled = false; |
||
234 | |||
235 | // Encrypt private key with user pwd as passphrase |
||
236 | $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase, Helper::getCipher()); |
||
237 | |||
238 | // Save key-pair |
||
239 | if ($encryptedPrivateKey) { |
||
240 | $header = crypt::generateHeader(); |
||
241 | $this->view->file_put_contents($this->privateKeyPath, $header . $encryptedPrivateKey); |
||
242 | $this->view->file_put_contents($this->publicKeyPath, $keypair['publicKey']); |
||
243 | } |
||
244 | |||
245 | \OC_FileProxy::$enabled = true; |
||
246 | } |
||
247 | |||
248 | } else { |
||
249 | // check if public-key exists but private-key is missing |
||
250 | if ($this->view->file_exists($this->publicKeyPath) && !$this->view->file_exists($this->privateKeyPath)) { |
||
251 | \OCP\Util::writeLog('Encryption library', |
||
252 | 'public key exists but private key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); |
||
253 | return false; |
||
254 | } else { |
||
255 | if (!$this->view->file_exists($this->publicKeyPath) && $this->view->file_exists($this->privateKeyPath) |
||
256 | ) { |
||
257 | \OCP\Util::writeLog('Encryption library', |
||
258 | 'private key exists but public key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); |
||
259 | return false; |
||
260 | } |
||
261 | } |
||
262 | } |
||
263 | |||
264 | return true; |
||
265 | |||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @return string |
||
270 | */ |
||
271 | public function getPublicShareKeyId() { |
||
272 | return $this->publicShareKeyId; |
||
273 | } |
||
274 | |||
275 | /** |
||
276 | * Check whether pwd recovery is enabled for a given user |
||
277 | * @return bool 1 = yes, 0 = no, false = no record |
||
278 | * |
||
279 | * @note If records are not being returned, check for a hidden space |
||
280 | * at the start of the uid in db |
||
281 | */ |
||
282 | public function recoveryEnabledForUser() { |
||
283 | |||
284 | $recoveryMode = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'recovery_enabled', '0'); |
||
285 | |||
286 | return ($recoveryMode === '1') ? true : false; |
||
287 | |||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Enable / disable pwd recovery for a given user |
||
292 | * @param bool $enabled Whether to enable or disable recovery |
||
293 | * @return bool |
||
294 | */ |
||
295 | public function setRecoveryForUser($enabled) { |
||
296 | |||
297 | $value = $enabled ? '1' : '0'; |
||
298 | try { |
||
299 | \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'recovery_enabled', $value); |
||
300 | return true; |
||
301 | } catch(\OCP\PreConditionNotMetException $e) { |
||
302 | return false; |
||
303 | } |
||
304 | |||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Find all files and their encryption status within a directory |
||
309 | * @param string $directory The path of the parent directory to search |
||
310 | * @param bool $found the founded files if called again |
||
311 | * @return array keys: plain, encrypted, broken |
||
312 | * @note $directory needs to be a path relative to OC data dir. e.g. |
||
313 | * /admin/files NOT /backup OR /home/www/oc/data/admin/files |
||
314 | */ |
||
315 | public function findEncFiles($directory, &$found = false) { |
||
316 | |||
317 | // Disable proxy - we don't want files to be decrypted before |
||
318 | // we handle them |
||
319 | \OC_FileProxy::$enabled = false; |
||
320 | |||
321 | if ($found === false) { |
||
322 | $found = array( |
||
323 | 'plain' => array(), |
||
324 | 'encrypted' => array(), |
||
325 | 'broken' => array(), |
||
326 | ); |
||
327 | } |
||
328 | |||
329 | if ($this->view->is_dir($directory) && $handle = $this->view->opendir($directory)){ |
||
330 | if (is_resource($handle)) { |
||
331 | while (false !== ($file = readdir($handle))) { |
||
332 | |||
333 | if ($file !== "." && $file !== "..") { |
||
334 | // skip stray part files |
||
335 | if (Helper::isPartialFilePath($file)) { |
||
336 | continue; |
||
337 | } |
||
338 | |||
339 | $filePath = $directory . '/' . $this->view->getRelativePath('/' . $file); |
||
340 | $relPath = Helper::stripUserFilesPath($filePath); |
||
341 | |||
342 | // If the path is a directory, search |
||
343 | // its contents |
||
344 | if ($this->view->is_dir($filePath)) { |
||
345 | |||
346 | $this->findEncFiles($filePath, $found); |
||
347 | |||
348 | // If the path is a file, determine |
||
349 | // its encryption status |
||
350 | } elseif ($this->view->is_file($filePath)) { |
||
351 | |||
352 | // Disable proxies again, some- |
||
353 | // where they got re-enabled :/ |
||
354 | \OC_FileProxy::$enabled = false; |
||
355 | |||
356 | $isEncryptedPath = $this->isEncryptedPath($filePath); |
||
357 | // If the file is encrypted |
||
358 | // NOTE: If the userId is |
||
359 | // empty or not set, file will |
||
360 | // detected as plain |
||
361 | // NOTE: This is inefficient; |
||
362 | // scanning every file like this |
||
363 | // will eat server resources :( |
||
364 | if ($isEncryptedPath) { |
||
365 | |||
366 | $fileKey = Keymanager::getFileKey($this->view, $this, $relPath); |
||
367 | $shareKey = Keymanager::getShareKey($this->view, $this->userId, $this, $relPath); |
||
368 | // if file is encrypted but now file key is available, throw exception |
||
369 | if ($fileKey === false || $shareKey === false) { |
||
370 | \OCP\Util::writeLog('encryption library', 'No keys available to decrypt the file: ' . $filePath, \OCP\Util::ERROR); |
||
371 | $found['broken'][] = array( |
||
372 | 'name' => $file, |
||
373 | 'path' => $filePath, |
||
374 | ); |
||
375 | } else { |
||
376 | $found['encrypted'][] = array( |
||
377 | 'name' => $file, |
||
378 | 'path' => $filePath, |
||
379 | ); |
||
380 | } |
||
381 | |||
382 | // If the file is not encrypted |
||
383 | } else { |
||
384 | |||
385 | $found['plain'][] = array( |
||
386 | 'name' => $file, |
||
387 | 'path' => $relPath |
||
388 | ); |
||
389 | } |
||
390 | } |
||
391 | } |
||
392 | } |
||
393 | } |
||
394 | } |
||
395 | |||
396 | \OC_FileProxy::$enabled = true; |
||
397 | |||
398 | return $found; |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * Check if a given path identifies an encrypted file |
||
403 | * @param string $path |
||
404 | * @return boolean |
||
405 | */ |
||
406 | public function isEncryptedPath($path) { |
||
407 | |||
408 | // Disable encryption proxy so data retrieved is in its |
||
409 | // original form |
||
410 | $proxyStatus = \OC_FileProxy::$enabled; |
||
411 | \OC_FileProxy::$enabled = false; |
||
412 | |||
413 | $data = ''; |
||
414 | |||
415 | // we only need 24 byte from the last chunk |
||
416 | if ($this->view->file_exists($path)) { |
||
417 | $handle = $this->view->fopen($path, 'r'); |
||
418 | if (is_resource($handle)) { |
||
419 | // suppress fseek warining, we handle the case that fseek doesn't |
||
420 | // work in the else branch |
||
421 | if (@fseek($handle, -24, SEEK_END) === 0) { |
||
422 | $data = fgets($handle); |
||
423 | } else { |
||
424 | // if fseek failed on the storage we create a local copy from the file |
||
425 | // and read this one |
||
426 | fclose($handle); |
||
427 | $localFile = $this->view->getLocalFile($path); |
||
428 | $handle = fopen($localFile, 'r'); |
||
429 | if (is_resource($handle) && fseek($handle, -24, SEEK_END) === 0) { |
||
430 | $data = fgets($handle); |
||
431 | } |
||
432 | } |
||
433 | fclose($handle); |
||
434 | } |
||
435 | } |
||
436 | |||
437 | // re-enable proxy |
||
438 | \OC_FileProxy::$enabled = $proxyStatus; |
||
439 | |||
440 | return Crypt::isCatfileContent($data); |
||
441 | } |
||
442 | |||
443 | /** |
||
444 | * get the file size of the unencrypted file |
||
445 | * @param string $path absolute path |
||
446 | * @return bool |
||
447 | */ |
||
448 | public function getFileSize($path) { |
||
449 | |||
450 | $result = 0; |
||
451 | |||
452 | // Disable encryption proxy to prevent recursive calls |
||
453 | $proxyStatus = \OC_FileProxy::$enabled; |
||
454 | \OC_FileProxy::$enabled = false; |
||
455 | |||
456 | // split the path parts |
||
457 | $pathParts = explode('/', $path); |
||
458 | |||
459 | if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path) |
||
460 | && $this->isEncryptedPath($path) |
||
461 | ) { |
||
462 | |||
463 | $cipher = 'AES-128-CFB'; |
||
464 | $realSize = 0; |
||
465 | |||
466 | // get the size from filesystem |
||
467 | $size = $this->view->filesize($path); |
||
468 | |||
469 | // open stream |
||
470 | $stream = $this->view->fopen($path, "r"); |
||
471 | |||
472 | if (is_resource($stream)) { |
||
473 | |||
474 | // if the file contains a encryption header we |
||
475 | // we set the cipher |
||
476 | // and we update the size |
||
477 | if ($this->containHeader($path)) { |
||
478 | $data = fread($stream,Crypt::BLOCKSIZE); |
||
479 | $header = Crypt::parseHeader($data); |
||
480 | $cipher = Crypt::getCipher($header); |
||
481 | $size -= Crypt::BLOCKSIZE; |
||
482 | } |
||
483 | |||
484 | // fast path, else the calculation for $lastChunkNr is bogus |
||
485 | if ($size === 0) { |
||
486 | \OC_FileProxy::$enabled = $proxyStatus; |
||
487 | return 0; |
||
488 | } |
||
489 | |||
490 | // calculate last chunk nr |
||
491 | // next highest is end of chunks, one subtracted is last one |
||
492 | // we have to read the last chunk, we can't just calculate it (because of padding etc) |
||
493 | $lastChunkNr = ceil($size/Crypt::BLOCKSIZE)-1; |
||
494 | |||
495 | // calculate last chunk position |
||
496 | $lastChunkPos = ($lastChunkNr * Crypt::BLOCKSIZE); |
||
497 | |||
498 | // get the content of the last chunk |
||
499 | if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) { |
||
500 | $realSize+=$lastChunkNr*6126; |
||
501 | } |
||
502 | $lastChunkContentEncrypted=''; |
||
503 | $count=Crypt::BLOCKSIZE; |
||
504 | while ($count>0) { |
||
505 | $data=fread($stream,Crypt::BLOCKSIZE); |
||
506 | $count=strlen($data); |
||
507 | $lastChunkContentEncrypted.=$data; |
||
508 | if(strlen($lastChunkContentEncrypted)>Crypt::BLOCKSIZE) { |
||
509 | $realSize+=6126; |
||
510 | $lastChunkContentEncrypted=substr($lastChunkContentEncrypted,Crypt::BLOCKSIZE); |
||
511 | } |
||
512 | } |
||
513 | fclose($stream); |
||
514 | $relPath = Helper::stripUserFilesPath($path); |
||
515 | $shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $relPath); |
||
516 | if($shareKey===false) { |
||
517 | \OC_FileProxy::$enabled = $proxyStatus; |
||
518 | return $result; |
||
519 | } |
||
520 | $session = new Session($this->view); |
||
521 | $privateKey = $session->getPrivateKey(); |
||
522 | $plainKeyfile = $this->decryptKeyfile($relPath, $privateKey); |
||
523 | $plainKey = Crypt::multiKeyDecrypt($plainKeyfile, $shareKey, $privateKey); |
||
524 | $lastChunkContent=Crypt::symmetricDecryptFileContent($lastChunkContentEncrypted, $plainKey, $cipher); |
||
525 | |||
526 | // calc the real file size with the size of the last chunk |
||
527 | $realSize += strlen($lastChunkContent); |
||
528 | |||
529 | // store file size |
||
530 | $result = $realSize; |
||
531 | } |
||
532 | } |
||
533 | |||
534 | \OC_FileProxy::$enabled = $proxyStatus; |
||
535 | |||
536 | return $result; |
||
537 | } |
||
538 | |||
539 | /** |
||
540 | * check if encrypted file contain a encryption header |
||
541 | * |
||
542 | * @param string $path |
||
543 | * @return boolean |
||
544 | */ |
||
545 | private function containHeader($path) { |
||
546 | // Disable encryption proxy to read the raw data |
||
547 | $proxyStatus = \OC_FileProxy::$enabled; |
||
548 | \OC_FileProxy::$enabled = false; |
||
549 | |||
550 | $isHeader = false; |
||
551 | $handle = $this->view->fopen($path, 'r'); |
||
552 | |||
553 | if (is_resource($handle)) { |
||
554 | $firstBlock = fread($handle, Crypt::BLOCKSIZE); |
||
555 | $isHeader = Crypt::isHeader($firstBlock); |
||
556 | } |
||
557 | |||
558 | \OC_FileProxy::$enabled = $proxyStatus; |
||
559 | |||
560 | return $isHeader; |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * fix the file size of the encrypted file |
||
565 | * @param string $path absolute path |
||
566 | * @return boolean true / false if file is encrypted |
||
567 | */ |
||
568 | public function fixFileSize($path) { |
||
569 | |||
570 | $result = false; |
||
571 | |||
572 | // Disable encryption proxy to prevent recursive calls |
||
573 | $proxyStatus = \OC_FileProxy::$enabled; |
||
574 | \OC_FileProxy::$enabled = false; |
||
575 | |||
576 | $realSize = $this->getFileSize($path); |
||
577 | |||
578 | if ($realSize > 0) { |
||
579 | |||
580 | $cached = $this->view->getFileInfo($path); |
||
581 | $cached['encrypted'] = true; |
||
582 | |||
583 | // set the size |
||
584 | $cached['unencrypted_size'] = $realSize; |
||
585 | |||
586 | // put file info |
||
587 | $this->view->putFileInfo($path, $cached); |
||
588 | |||
589 | $result = true; |
||
590 | |||
591 | } |
||
592 | |||
593 | \OC_FileProxy::$enabled = $proxyStatus; |
||
594 | |||
595 | return $result; |
||
596 | } |
||
597 | |||
598 | /** |
||
599 | * encrypt versions from given file |
||
600 | * @param array $filelist list of encrypted files, relative to data/user/files |
||
601 | * @return boolean |
||
602 | */ |
||
603 | View Code Duplication | private function encryptVersions($filelist) { |
|
604 | |||
605 | $successful = true; |
||
606 | |||
607 | if (\OCP\App::isEnabled('files_versions')) { |
||
608 | |||
609 | foreach ($filelist as $filename) { |
||
610 | |||
611 | $versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename); |
||
612 | foreach ($versions as $version) { |
||
613 | |||
614 | $path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version']; |
||
615 | |||
616 | $encHandle = fopen('crypt://' . $path . '.part', 'wb'); |
||
617 | |||
618 | if ($encHandle === false) { |
||
619 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL); |
||
620 | $successful = false; |
||
621 | continue; |
||
622 | } |
||
623 | |||
624 | $plainHandle = $this->view->fopen($path, 'rb'); |
||
625 | if ($plainHandle === false) { |
||
626 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL); |
||
627 | $successful = false; |
||
628 | continue; |
||
629 | } |
||
630 | |||
631 | stream_copy_to_stream($plainHandle, $encHandle); |
||
632 | |||
633 | fclose($encHandle); |
||
634 | fclose($plainHandle); |
||
635 | |||
636 | $this->view->rename($path . '.part', $path); |
||
637 | } |
||
638 | } |
||
639 | } |
||
640 | |||
641 | return $successful; |
||
642 | } |
||
643 | |||
644 | /** |
||
645 | * decrypt versions from given file |
||
646 | * @param string $filelist list of decrypted files, relative to data/user/files |
||
647 | * @return boolean |
||
648 | */ |
||
649 | View Code Duplication | private function decryptVersions($filelist) { |
|
650 | |||
651 | $successful = true; |
||
652 | |||
653 | if (\OCP\App::isEnabled('files_versions')) { |
||
654 | |||
655 | foreach ($filelist as $filename) { |
||
656 | |||
657 | $versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename); |
||
658 | foreach ($versions as $version) { |
||
659 | |||
660 | $path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version']; |
||
661 | |||
662 | $encHandle = fopen('crypt://' . $path, 'rb'); |
||
663 | |||
664 | if ($encHandle === false) { |
||
665 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL); |
||
666 | $successful = false; |
||
667 | continue; |
||
668 | } |
||
669 | |||
670 | $plainHandle = $this->view->fopen($path . '.part', 'wb'); |
||
671 | if ($plainHandle === false) { |
||
672 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL); |
||
673 | $successful = false; |
||
674 | continue; |
||
675 | } |
||
676 | |||
677 | stream_copy_to_stream($encHandle, $plainHandle); |
||
678 | |||
679 | fclose($encHandle); |
||
680 | fclose($plainHandle); |
||
681 | |||
682 | $this->view->rename($path . '.part', $path); |
||
683 | } |
||
684 | } |
||
685 | } |
||
686 | |||
687 | return $successful; |
||
688 | } |
||
689 | |||
690 | /** |
||
691 | * Decrypt all files |
||
692 | * @return bool |
||
693 | */ |
||
694 | public function decryptAll() { |
||
695 | |||
696 | $found = $this->findEncFiles($this->userId . '/files'); |
||
697 | |||
698 | $successful = true; |
||
699 | |||
700 | if ($found) { |
||
701 | |||
702 | $versionStatus = \OCP\App::isEnabled('files_versions'); |
||
703 | \OC_App::disable('files_versions'); |
||
704 | |||
705 | $decryptedFiles = array(); |
||
706 | |||
707 | // Encrypt unencrypted files |
||
708 | foreach ($found['encrypted'] as $encryptedFile) { |
||
709 | |||
710 | //relative to data/<user>/file |
||
711 | $relPath = Helper::stripUserFilesPath($encryptedFile['path']); |
||
712 | |||
713 | //get file info |
||
714 | $fileInfo = \OC\Files\Filesystem::getFileInfo($relPath); |
||
715 | |||
716 | //relative to /data |
||
717 | $rawPath = $encryptedFile['path']; |
||
718 | |||
719 | //get timestamp |
||
720 | $timestamp = $fileInfo['mtime']; |
||
721 | |||
722 | //enable proxy to use OC\Files\View to access the original file |
||
723 | \OC_FileProxy::$enabled = true; |
||
724 | |||
725 | // Open enc file handle for binary reading |
||
726 | $encHandle = $this->view->fopen($rawPath, 'rb'); |
||
727 | |||
728 | // Disable proxy to prevent file being encrypted again |
||
729 | \OC_FileProxy::$enabled = false; |
||
730 | |||
731 | View Code Duplication | if ($encHandle === false) { |
|
732 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL); |
||
733 | $successful = false; |
||
734 | continue; |
||
735 | } |
||
736 | |||
737 | // Open plain file handle for binary writing, with same filename as original plain file |
||
738 | $plainHandle = $this->view->fopen($rawPath . '.part', 'wb'); |
||
739 | View Code Duplication | if ($plainHandle === false) { |
|
740 | \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '.part", decryption failed!', \OCP\Util::FATAL); |
||
741 | $successful = false; |
||
742 | continue; |
||
743 | } |
||
744 | |||
745 | // Move plain file to a temporary location |
||
746 | $size = stream_copy_to_stream($encHandle, $plainHandle); |
||
747 | View Code Duplication | if ($size === 0) { |
|
748 | \OCP\Util::writeLog('Encryption library', 'Zero bytes copied of "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL); |
||
749 | $successful = false; |
||
750 | continue; |
||
751 | } |
||
752 | |||
753 | fclose($encHandle); |
||
754 | fclose($plainHandle); |
||
755 | |||
756 | $fakeRoot = $this->view->getRoot(); |
||
757 | $this->view->chroot('/' . $this->userId . '/files'); |
||
758 | |||
759 | $this->view->rename($relPath . '.part', $relPath); |
||
760 | |||
761 | //set timestamp |
||
762 | $this->view->touch($relPath, $timestamp); |
||
763 | |||
764 | $this->view->chroot($fakeRoot); |
||
765 | |||
766 | // Add the file to the cache |
||
767 | \OC\Files\Filesystem::putFileInfo($relPath, array( |
||
768 | 'encrypted' => false, |
||
769 | 'size' => $size, |
||
770 | 'unencrypted_size' => 0, |
||
771 | 'etag' => $fileInfo['etag'] |
||
772 | )); |
||
773 | |||
774 | $decryptedFiles[] = $relPath; |
||
775 | |||
776 | } |
||
777 | |||
778 | if ($versionStatus) { |
||
779 | \OC_App::enable('files_versions'); |
||
780 | } |
||
781 | |||
782 | if (!$this->decryptVersions($decryptedFiles)) { |
||
783 | $successful = false; |
||
784 | } |
||
785 | |||
786 | // if there are broken encrypted files than the complete decryption |
||
787 | // was not successful |
||
788 | if (!empty($found['broken'])) { |
||
789 | $successful = false; |
||
790 | } |
||
791 | |||
792 | if ($successful) { |
||
793 | $this->backupAllKeys('decryptAll', false, false); |
||
794 | $this->view->deleteAll($this->keysPath); |
||
795 | } |
||
796 | |||
797 | \OC_FileProxy::$enabled = true; |
||
798 | } |
||
799 | |||
800 | return $successful; |
||
801 | } |
||
802 | |||
803 | /** |
||
804 | * Encrypt all files in a directory |
||
805 | * @param string $dirPath the directory whose files will be encrypted |
||
806 | * @return bool |
||
807 | * @note Encryption is recursive |
||
808 | */ |
||
809 | public function encryptAll($dirPath) { |
||
810 | |||
811 | $result = true; |
||
812 | |||
813 | $found = $this->findEncFiles($dirPath); |
||
814 | |||
815 | // Disable proxy to prevent file being encrypted twice |
||
816 | \OC_FileProxy::$enabled = false; |
||
817 | |||
818 | $versionStatus = \OCP\App::isEnabled('files_versions'); |
||
819 | \OC_App::disable('files_versions'); |
||
820 | |||
821 | $encryptedFiles = array(); |
||
822 | |||
823 | // Encrypt unencrypted files |
||
824 | foreach ($found['plain'] as $plainFile) { |
||
825 | |||
826 | //get file info |
||
827 | $fileInfo = \OC\Files\Filesystem::getFileInfo($plainFile['path']); |
||
828 | |||
829 | //relative to data/<user>/file |
||
830 | $relPath = $plainFile['path']; |
||
831 | |||
832 | //relative to /data |
||
833 | $rawPath = '/' . $this->userId . '/files/' . $plainFile['path']; |
||
834 | |||
835 | // keep timestamp |
||
836 | $timestamp = $fileInfo['mtime']; |
||
837 | |||
838 | // Open plain file handle for binary reading |
||
839 | $plainHandle = $this->view->fopen($rawPath, 'rb'); |
||
840 | |||
841 | // Open enc file handle for binary writing, with same filename as original plain file |
||
842 | $encHandle = fopen('crypt://' . $rawPath . '.part', 'wb'); |
||
843 | |||
844 | if (is_resource($encHandle) && is_resource($plainHandle)) { |
||
845 | // Move plain file to a temporary location |
||
846 | $size = stream_copy_to_stream($plainHandle, $encHandle); |
||
847 | |||
848 | fclose($encHandle); |
||
849 | fclose($plainHandle); |
||
850 | |||
851 | $fakeRoot = $this->view->getRoot(); |
||
852 | $this->view->chroot('/' . $this->userId . '/files'); |
||
853 | |||
854 | $this->view->rename($relPath . '.part', $relPath); |
||
855 | |||
856 | // set timestamp |
||
857 | $this->view->touch($relPath, $timestamp); |
||
858 | |||
859 | $encSize = $this->view->filesize($relPath); |
||
860 | |||
861 | $this->view->chroot($fakeRoot); |
||
862 | |||
863 | // Add the file to the cache |
||
864 | \OC\Files\Filesystem::putFileInfo($relPath, array( |
||
865 | 'encrypted' => true, |
||
866 | 'size' => $encSize, |
||
867 | 'unencrypted_size' => $size, |
||
868 | 'etag' => $fileInfo['etag'] |
||
869 | )); |
||
870 | |||
871 | $encryptedFiles[] = $relPath; |
||
872 | } else { |
||
873 | \OCP\Util::writeLog('files_encryption', 'initial encryption: could not encrypt ' . $rawPath, \OCP\Util::FATAL); |
||
874 | $result = false; |
||
875 | } |
||
876 | } |
||
877 | |||
878 | \OC_FileProxy::$enabled = true; |
||
879 | |||
880 | if ($versionStatus) { |
||
881 | \OC_App::enable('files_versions'); |
||
882 | } |
||
883 | |||
884 | $result = $result && $this->encryptVersions($encryptedFiles); |
||
885 | |||
886 | return $result; |
||
887 | |||
888 | } |
||
889 | |||
890 | /** |
||
891 | * Return important encryption related paths |
||
892 | * @param string $pathName Name of the directory to return the path of |
||
893 | * @return string path |
||
894 | */ |
||
895 | public function getPath($pathName) { |
||
896 | |||
897 | switch ($pathName) { |
||
898 | |||
899 | case 'publicKeyDir': |
||
900 | |||
901 | return $this->publicKeyDir; |
||
902 | |||
903 | break; |
||
904 | |||
905 | case 'encryptionDir': |
||
906 | |||
907 | return $this->encryptionDir; |
||
908 | |||
909 | break; |
||
910 | |||
911 | case 'keysPath': |
||
912 | |||
913 | return $this->keysPath; |
||
914 | |||
915 | break; |
||
916 | |||
917 | case 'publicKeyPath': |
||
918 | |||
919 | return $this->publicKeyPath; |
||
920 | |||
921 | break; |
||
922 | |||
923 | case 'privateKeyPath': |
||
924 | |||
925 | return $this->privateKeyPath; |
||
926 | |||
927 | break; |
||
928 | } |
||
929 | |||
930 | return false; |
||
931 | |||
932 | } |
||
933 | |||
934 | /** |
||
935 | * Returns whether the given user is ready for encryption. |
||
936 | * Also returns true if the given user is the public user |
||
937 | * or the recovery key user. |
||
938 | * |
||
939 | * @param string $user user to check |
||
940 | * |
||
941 | * @return boolean true if the user is ready, false otherwise |
||
942 | */ |
||
943 | private function isUserReady($user) { |
||
944 | if ($user === $this->publicShareKeyId |
||
945 | || $user === $this->recoveryKeyId |
||
946 | ) { |
||
947 | return true; |
||
948 | } |
||
949 | try { |
||
950 | $util = new Util($this->view, $user); |
||
951 | return $util->ready(); |
||
952 | } catch (NoUserException $e) { |
||
953 | \OCP\Util::writeLog('Encryption library', |
||
954 | 'No User object for '.$user, \OCP\Util::DEBUG); |
||
955 | return false; |
||
956 | } |
||
957 | } |
||
958 | |||
959 | /** |
||
960 | * Filter an array of UIDs to return only ones ready for sharing |
||
961 | * @param array $unfilteredUsers users to be checked for sharing readiness |
||
962 | * @return array as multi-dimensional array. keys: ready, unready |
||
963 | */ |
||
964 | public function filterShareReadyUsers($unfilteredUsers) { |
||
965 | |||
966 | // This array will collect the filtered IDs |
||
967 | $readyIds = $unreadyIds = array(); |
||
968 | |||
969 | // Loop through users and create array of UIDs that need new keyfiles |
||
970 | foreach ($unfilteredUsers as $user) { |
||
971 | // Check that the user is encryption capable, or is the |
||
972 | // public system user (for public shares) |
||
973 | if ($this->isUserReady($user)) { |
||
974 | |||
975 | // Construct array of ready UIDs for Keymanager{} |
||
976 | $readyIds[] = $user; |
||
977 | |||
978 | } else { |
||
979 | |||
980 | // Construct array of unready UIDs for Keymanager{} |
||
981 | $unreadyIds[] = $user; |
||
982 | |||
983 | // Log warning; we can't do necessary setup here |
||
984 | // because we don't have the user passphrase |
||
985 | \OCP\Util::writeLog('Encryption library', |
||
986 | '"' . $user . '" is not setup for encryption', \OCP\Util::WARN); |
||
987 | |||
988 | } |
||
989 | |||
990 | } |
||
991 | |||
992 | return array( |
||
993 | 'ready' => $readyIds, |
||
994 | 'unready' => $unreadyIds |
||
995 | ); |
||
996 | |||
997 | } |
||
998 | |||
999 | /** |
||
1000 | * Decrypt a keyfile |
||
1001 | * @param string $filePath |
||
1002 | * @param string $privateKey |
||
1003 | * @return false|string |
||
1004 | */ |
||
1005 | private function decryptKeyfile($filePath, $privateKey) { |
||
1006 | |||
1007 | // Get the encrypted keyfile |
||
1008 | $encKeyfile = Keymanager::getFileKey($this->view, $this, $filePath); |
||
1009 | |||
1010 | // The file has a shareKey and must use it for decryption |
||
1011 | $shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $filePath); |
||
1012 | |||
1013 | $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); |
||
1014 | |||
1015 | return $plainKeyfile; |
||
1016 | } |
||
1017 | |||
1018 | /** |
||
1019 | * Encrypt keyfile to multiple users |
||
1020 | * @param Session $session |
||
1021 | * @param array $users list of users which should be able to access the file |
||
1022 | * @param string $filePath path of the file to be shared |
||
1023 | * @return bool |
||
1024 | */ |
||
1025 | public function setSharedFileKeyfiles(Session $session, array $users, $filePath) { |
||
1026 | |||
1027 | // Make sure users are capable of sharing |
||
1028 | $filteredUids = $this->filterShareReadyUsers($users); |
||
1029 | |||
1030 | // If we're attempting to share to unready users |
||
1031 | if (!empty($filteredUids['unready'])) { |
||
1032 | |||
1033 | \OCP\Util::writeLog('Encryption library', |
||
1034 | 'Sharing to these user(s) failed as they are unready for encryption:"' |
||
1035 | . print_r($filteredUids['unready'], 1), \OCP\Util::WARN); |
||
1036 | |||
1037 | return false; |
||
1038 | |||
1039 | } |
||
1040 | |||
1041 | // Get public keys for each user, ready for generating sharekeys |
||
1042 | $userPubKeys = Keymanager::getPublicKeys($this->view, $filteredUids['ready']); |
||
1043 | |||
1044 | // Note proxy status then disable it |
||
1045 | $proxyStatus = \OC_FileProxy::$enabled; |
||
1046 | \OC_FileProxy::$enabled = false; |
||
1047 | |||
1048 | // Get the current users's private key for decrypting existing keyfile |
||
1049 | $privateKey = $session->getPrivateKey(); |
||
1050 | |||
1051 | try { |
||
1052 | // Decrypt keyfile |
||
1053 | $plainKeyfile = $this->decryptKeyfile($filePath, $privateKey); |
||
1054 | // Re-enc keyfile to (additional) sharekeys |
||
1055 | $multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys); |
||
1056 | } catch (Exception\EncryptionException $e) { |
||
1057 | $msg = 'set shareFileKeyFailed (code: ' . $e->getCode() . '): ' . $e->getMessage(); |
||
1058 | \OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL); |
||
1059 | return false; |
||
1060 | } catch (\Exception $e) { |
||
1061 | $msg = 'set shareFileKeyFailed (unknown error): ' . $e->getMessage(); |
||
1062 | \OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL); |
||
1063 | return false; |
||
1064 | } |
||
1065 | |||
1066 | // Save the recrypted key to it's owner's keyfiles directory |
||
1067 | // Save new sharekeys to all necessary user directory |
||
1068 | if ( |
||
1069 | !Keymanager::setFileKey($this->view, $this, $filePath, $multiEncKey['data']) |
||
1070 | || !Keymanager::setShareKeys($this->view, $this, $filePath, $multiEncKey['keys']) |
||
1071 | ) { |
||
1072 | |||
1073 | \OCP\Util::writeLog('Encryption library', |
||
1074 | 'Keyfiles could not be saved for users sharing ' . $filePath, \OCP\Util::ERROR); |
||
1075 | |||
1076 | return false; |
||
1077 | |||
1078 | } |
||
1079 | |||
1080 | // Return proxy to original status |
||
1081 | \OC_FileProxy::$enabled = $proxyStatus; |
||
1082 | |||
1083 | return true; |
||
1084 | } |
||
1085 | |||
1086 | /** |
||
1087 | * Find, sanitise and format users sharing a file |
||
1088 | * @note This wraps other methods into a portable bundle |
||
1089 | * @param boolean $sharingEnabled |
||
1090 | * @param string $filePath path relativ to current users files folder |
||
1091 | */ |
||
1092 | public function getSharingUsersArray($sharingEnabled, $filePath) { |
||
1093 | |||
1094 | $appConfig = \OC::$server->getAppConfig(); |
||
1095 | |||
1096 | // Check if key recovery is enabled |
||
1097 | if ( |
||
1098 | $appConfig->getValue('files_encryption', 'recoveryAdminEnabled') |
||
1099 | && $this->recoveryEnabledForUser() |
||
1100 | ) { |
||
1101 | $recoveryEnabled = true; |
||
1102 | } else { |
||
1103 | $recoveryEnabled = false; |
||
1104 | } |
||
1105 | |||
1106 | // Make sure that a share key is generated for the owner too |
||
1107 | list($owner, $ownerPath) = $this->getUidAndFilename($filePath); |
||
1108 | |||
1109 | $ownerPath = Helper::stripPartialFileExtension($ownerPath); |
||
1110 | |||
1111 | // always add owner to the list of users with access to the file |
||
1112 | $userIds = array($owner); |
||
1113 | |||
1114 | if ($sharingEnabled) { |
||
1115 | |||
1116 | // Find out who, if anyone, is sharing the file |
||
1117 | $result = \OCP\Share::getUsersSharingFile($ownerPath, $owner); |
||
1118 | $userIds = \array_merge($userIds, $result['users']); |
||
1119 | if ($result['public'] || $result['remote']) { |
||
1120 | $userIds[] = $this->publicShareKeyId; |
||
1121 | } |
||
1122 | |||
1123 | } |
||
1124 | |||
1125 | // If recovery is enabled, add the |
||
1126 | // Admin UID to list of users to share to |
||
1127 | if ($recoveryEnabled) { |
||
1128 | // Find recoveryAdmin user ID |
||
1129 | $recoveryKeyId = $appConfig->getValue('files_encryption', 'recoveryKeyId'); |
||
1130 | // Add recoveryAdmin to list of users sharing |
||
1131 | $userIds[] = $recoveryKeyId; |
||
1132 | } |
||
1133 | |||
1134 | // check if it is a group mount |
||
1135 | if (\OCP\App::isEnabled("files_external")) { |
||
1136 | $mounts = \OC_Mount_Config::getSystemMountPoints(); |
||
1137 | foreach ($mounts as $mount) { |
||
1138 | if ($mount['mountpoint'] == substr($ownerPath, 1, strlen($mount['mountpoint']))) { |
||
1139 | $userIds = array_merge($userIds, $this->getUserWithAccessToMountPoint($mount['applicable']['users'], $mount['applicable']['groups'])); |
||
1140 | } |
||
1141 | } |
||
1142 | } |
||
1143 | |||
1144 | // Remove duplicate UIDs |
||
1145 | $uniqueUserIds = array_unique($userIds); |
||
1146 | |||
1147 | return $uniqueUserIds; |
||
1148 | |||
1149 | } |
||
1150 | |||
1151 | private function getUserWithAccessToMountPoint($users, $groups) { |
||
1152 | $result = array(); |
||
1153 | if (in_array('all', $users)) { |
||
1154 | $result = \OCP\User::getUsers(); |
||
1155 | } else { |
||
1156 | $result = array_merge($result, $users); |
||
1157 | foreach ($groups as $group) { |
||
1158 | $result = array_merge($result, \OC_Group::usersInGroup($group)); |
||
1159 | } |
||
1160 | } |
||
1161 | |||
1162 | return $result; |
||
1163 | } |
||
1164 | |||
1165 | /** |
||
1166 | * set migration status |
||
1167 | * @param int $status |
||
1168 | * @param int $preCondition only update migration status if the previous value equals $preCondition |
||
1169 | * @return boolean |
||
1170 | */ |
||
1171 | private function setMigrationStatus($status, $preCondition = null) { |
||
1172 | |||
1173 | // convert to string if preCondition is set |
||
1174 | $preCondition = ($preCondition === null) ? null : (string)$preCondition; |
||
1175 | |||
1176 | try { |
||
1177 | \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)$status, $preCondition); |
||
1178 | return true; |
||
1179 | } catch(\OCP\PreConditionNotMetException $e) { |
||
1180 | return false; |
||
1181 | } |
||
1182 | |||
1183 | } |
||
1184 | |||
1185 | /** |
||
1186 | * start migration mode to initially encrypt users data |
||
1187 | * @return boolean |
||
1188 | */ |
||
1189 | View Code Duplication | public function beginMigration() { |
|
1190 | |||
1191 | $result = $this->setMigrationStatus(self::MIGRATION_IN_PROGRESS, self::MIGRATION_OPEN); |
||
1192 | |||
1193 | if ($result) { |
||
1194 | \OCP\Util::writeLog('Encryption library', "Start migration to encryption mode for " . $this->userId, \OCP\Util::INFO); |
||
1195 | } else { |
||
1196 | \OCP\Util::writeLog('Encryption library', "Could not activate migration mode for " . $this->userId . ". Probably another process already started the initial encryption", \OCP\Util::WARN); |
||
1197 | } |
||
1198 | |||
1199 | return $result; |
||
1200 | } |
||
1201 | |||
1202 | public function resetMigrationStatus() { |
||
1203 | return $this->setMigrationStatus(self::MIGRATION_OPEN); |
||
1204 | |||
1205 | } |
||
1206 | |||
1207 | /** |
||
1208 | * close migration mode after users data has been encrypted successfully |
||
1209 | * @return boolean |
||
1210 | */ |
||
1211 | View Code Duplication | public function finishMigration() { |
|
1212 | $result = $this->setMigrationStatus(self::MIGRATION_COMPLETED); |
||
1213 | |||
1214 | if ($result) { |
||
1215 | \OCP\Util::writeLog('Encryption library', "Finish migration successfully for " . $this->userId, \OCP\Util::INFO); |
||
1216 | } else { |
||
1217 | \OCP\Util::writeLog('Encryption library', "Could not deactivate migration mode for " . $this->userId, \OCP\Util::WARN); |
||
1218 | } |
||
1219 | |||
1220 | return $result; |
||
1221 | } |
||
1222 | |||
1223 | /** |
||
1224 | * check if files are already migrated to the encryption system |
||
1225 | * @return int|false migration status, false = in case of no record |
||
1226 | * @note If records are not being returned, check for a hidden space |
||
1227 | * at the start of the uid in db |
||
1228 | */ |
||
1229 | public function getMigrationStatus() { |
||
1230 | |||
1231 | $migrationStatus = false; |
||
1232 | if (\OCP\User::userExists($this->userId)) { |
||
1233 | $migrationStatus = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'migration_status', null); |
||
1234 | if ($migrationStatus === null) { |
||
1235 | \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)self::MIGRATION_OPEN); |
||
1236 | $migrationStatus = self::MIGRATION_OPEN; |
||
1237 | } |
||
1238 | } |
||
1239 | |||
1240 | return (int)$migrationStatus; |
||
1241 | |||
1242 | } |
||
1243 | |||
1244 | /** |
||
1245 | * get uid of the owners of the file and the path to the file |
||
1246 | * @param string $path Path of the file to check |
||
1247 | * @throws \Exception |
||
1248 | * @note $shareFilePath must be relative to data/UID/files. Files |
||
1249 | * relative to /Shared are also acceptable |
||
1250 | * @return array |
||
1251 | */ |
||
1252 | public function getUidAndFilename($path) { |
||
1253 | |||
1254 | $pathinfo = pathinfo($path); |
||
1255 | $partfile = false; |
||
1256 | $parentFolder = false; |
||
1257 | if (array_key_exists('extension', $pathinfo) && $pathinfo['extension'] === 'part') { |
||
1258 | // if the real file exists we check this file |
||
1259 | $filePath = $this->userFilesDir . '/' .$pathinfo['dirname'] . '/' . $pathinfo['filename']; |
||
1260 | if ($this->view->file_exists($filePath)) { |
||
1261 | $pathToCheck = $pathinfo['dirname'] . '/' . $pathinfo['filename']; |
||
1262 | } else { // otherwise we look for the parent |
||
1263 | $pathToCheck = $pathinfo['dirname']; |
||
1264 | $parentFolder = true; |
||
1265 | } |
||
1266 | $partfile = true; |
||
1267 | } else { |
||
1268 | $pathToCheck = $path; |
||
1269 | } |
||
1270 | |||
1271 | $view = new \OC\Files\View($this->userFilesDir); |
||
1272 | $fileOwnerUid = $view->getOwner($pathToCheck); |
||
1273 | |||
1274 | // handle public access |
||
1275 | if ($this->isPublic) { |
||
1276 | return array($this->userId, $path); |
||
1277 | } else { |
||
1278 | |||
1279 | // Check that UID is valid |
||
1280 | if (!\OCP\User::userExists($fileOwnerUid)) { |
||
1281 | throw new \Exception( |
||
1282 | 'Could not find owner (UID = "' . var_export($fileOwnerUid, 1) . '") of file "' . $path . '"'); |
||
1283 | } |
||
1284 | |||
1285 | // NOTE: Bah, this dependency should be elsewhere |
||
1286 | \OC\Files\Filesystem::initMountPoints($fileOwnerUid); |
||
1287 | |||
1288 | // If the file owner is the currently logged in user |
||
1289 | if ($fileOwnerUid === $this->userId) { |
||
1290 | |||
1291 | // Assume the path supplied is correct |
||
1292 | $filename = $path; |
||
1293 | |||
1294 | } else { |
||
1295 | $info = $view->getFileInfo($pathToCheck); |
||
1296 | $ownerView = new \OC\Files\View('/' . $fileOwnerUid . '/files'); |
||
1297 | |||
1298 | // Fetch real file path from DB |
||
1299 | $filename = $ownerView->getPath($info['fileid']); |
||
1300 | if ($parentFolder) { |
||
1301 | $filename = $filename . '/'. $pathinfo['filename']; |
||
1302 | } |
||
1303 | |||
1304 | if ($partfile) { |
||
1305 | $filename = $filename . '.' . $pathinfo['extension']; |
||
1306 | } |
||
1307 | |||
1308 | } |
||
1309 | |||
1310 | return array( |
||
1311 | $fileOwnerUid, |
||
1312 | \OC\Files\Filesystem::normalizePath($filename) |
||
1313 | ); |
||
1314 | } |
||
1315 | } |
||
1316 | |||
1317 | /** |
||
1318 | * go recursively through a dir and collect all files and sub files. |
||
1319 | * @param string $dir relative to the users files folder |
||
1320 | * @return array with list of files relative to the users files folder |
||
1321 | */ |
||
1322 | public function getAllFiles($dir, $mountPoint = '') { |
||
1323 | $result = array(); |
||
1324 | $dirList = array($dir); |
||
1325 | |||
1326 | while ($dirList) { |
||
1327 | $dir = array_pop($dirList); |
||
1328 | $content = $this->view->getDirectoryContent(\OC\Files\Filesystem::normalizePath( |
||
1329 | $this->userFilesDir . '/' . $dir)); |
||
1330 | |||
1331 | foreach ($content as $c) { |
||
1332 | // getDirectoryContent() returns the paths relative to the mount points, so we need |
||
1333 | // to re-construct the complete path |
||
1334 | $path = ($mountPoint !== '') ? $mountPoint . '/' . $c['path'] : $c['path']; |
||
1335 | $path = \OC\Files\Filesystem::normalizePath($path); |
||
1336 | if ($c['type'] === 'dir') { |
||
1337 | $dirList[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files")); |
||
1338 | } else { |
||
1339 | $result[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files")); |
||
1340 | } |
||
1341 | } |
||
1342 | |||
1343 | } |
||
1344 | |||
1345 | return $result; |
||
1346 | } |
||
1347 | |||
1348 | /** |
||
1349 | * get owner of the shared files. |
||
1350 | * @param int $id ID of a share |
||
1351 | * @return string owner |
||
1352 | */ |
||
1353 | public function getOwnerFromSharedFile($id) { |
||
1354 | |||
1355 | $query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1); |
||
1356 | |||
1357 | $result = $query->execute(array($id)); |
||
1358 | |||
1359 | $source = null; |
||
1360 | if (\OCP\DB::isError($result)) { |
||
1361 | \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); |
||
1362 | } else { |
||
1363 | $source = $result->fetchRow(); |
||
1364 | } |
||
1365 | |||
1366 | $fileOwner = false; |
||
1367 | |||
1368 | if ($source && isset($source['parent'])) { |
||
1369 | |||
1370 | $parent = $source['parent']; |
||
1371 | |||
1372 | while (isset($parent)) { |
||
1373 | |||
1374 | $query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1); |
||
1375 | |||
1376 | $result = $query->execute(array($parent)); |
||
1377 | |||
1378 | $item = null; |
||
1379 | if (\OCP\DB::isError($result)) { |
||
1380 | \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); |
||
1381 | } else { |
||
1382 | $item = $result->fetchRow(); |
||
1383 | } |
||
1384 | |||
1385 | if ($item && isset($item['parent'])) { |
||
1386 | |||
1387 | $parent = $item['parent']; |
||
1388 | |||
1389 | } else { |
||
1390 | |||
1391 | $fileOwner = $item['uid_owner']; |
||
1392 | |||
1393 | break; |
||
1394 | |||
1395 | } |
||
1396 | } |
||
1397 | |||
1398 | } else { |
||
1399 | |||
1400 | $fileOwner = $source['uid_owner']; |
||
1401 | |||
1402 | } |
||
1403 | |||
1404 | return $fileOwner; |
||
1405 | |||
1406 | } |
||
1407 | |||
1408 | /** |
||
1409 | * @return string |
||
1410 | */ |
||
1411 | public function getUserId() { |
||
1412 | return $this->userId; |
||
1413 | } |
||
1414 | |||
1415 | /** |
||
1416 | * @return string |
||
1417 | */ |
||
1418 | public function getKeyId() { |
||
1419 | return $this->keyId; |
||
1420 | } |
||
1421 | |||
1422 | /** |
||
1423 | * @return string |
||
1424 | */ |
||
1425 | public function getUserFilesDir() { |
||
1426 | return $this->userFilesDir; |
||
1427 | } |
||
1428 | |||
1429 | /** |
||
1430 | * @param string $password |
||
1431 | * @return bool |
||
1432 | */ |
||
1433 | public function checkRecoveryPassword($password) { |
||
1434 | |||
1435 | $result = false; |
||
1436 | |||
1437 | $recoveryKey = Keymanager::getPrivateSystemKey($this->recoveryKeyId); |
||
1438 | $decryptedRecoveryKey = Crypt::decryptPrivateKey($recoveryKey, $password); |
||
1439 | |||
1440 | if ($decryptedRecoveryKey) { |
||
1441 | $result = true; |
||
1442 | } |
||
1443 | |||
1444 | return $result; |
||
1445 | } |
||
1446 | |||
1447 | /** |
||
1448 | * @return string |
||
1449 | */ |
||
1450 | public function getRecoveryKeyId() { |
||
1451 | return $this->recoveryKeyId; |
||
1452 | } |
||
1453 | |||
1454 | /** |
||
1455 | * add recovery key to all encrypted files |
||
1456 | */ |
||
1457 | public function addRecoveryKeys($path = '/') { |
||
1458 | $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); |
||
1459 | foreach ($dirContent as $item) { |
||
1460 | // get relative path from files_encryption/keyfiles/ |
||
1461 | $filePath = substr($item['path'], strlen('files_encryption/keys')); |
||
1462 | if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { |
||
1463 | $this->addRecoveryKeys($filePath . '/'); |
||
1464 | } else { |
||
1465 | $session = new Session(new \OC\Files\View('/')); |
||
1466 | $sharingEnabled = \OCP\Share::isEnabled(); |
||
1467 | $usersSharing = $this->getSharingUsersArray($sharingEnabled, $filePath); |
||
1468 | $this->setSharedFileKeyfiles($session, $usersSharing, $filePath); |
||
1469 | } |
||
1470 | } |
||
1471 | } |
||
1472 | |||
1473 | /** |
||
1474 | * remove recovery key to all encrypted files |
||
1475 | */ |
||
1476 | public function removeRecoveryKeys($path = '/') { |
||
1477 | $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); |
||
1478 | foreach ($dirContent as $item) { |
||
1479 | // get relative path from files_encryption/keyfiles |
||
1480 | $filePath = substr($item['path'], strlen('files_encryption/keys')); |
||
1481 | if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { |
||
1482 | $this->removeRecoveryKeys($filePath . '/'); |
||
1483 | } else { |
||
1484 | $this->view->unlink($this->keysPath . '/' . $filePath . '/' . $this->recoveryKeyId . '.shareKey'); |
||
1485 | } |
||
1486 | } |
||
1487 | } |
||
1488 | |||
1489 | /** |
||
1490 | * decrypt given file with recovery key and encrypt it again to the owner and his new key |
||
1491 | * @param string $file |
||
1492 | * @param string $privateKey recovery key to decrypt the file |
||
1493 | */ |
||
1494 | private function recoverFile($file, $privateKey) { |
||
1495 | |||
1496 | $sharingEnabled = \OCP\Share::isEnabled(); |
||
1497 | |||
1498 | // Find out who, if anyone, is sharing the file |
||
1499 | if ($sharingEnabled) { |
||
1500 | $result = \OCP\Share::getUsersSharingFile($file, $this->userId, true); |
||
1501 | $userIds = $result['users']; |
||
1502 | $userIds[] = $this->recoveryKeyId; |
||
1503 | if ($result['public']) { |
||
1504 | $userIds[] = $this->publicShareKeyId; |
||
1505 | } |
||
1506 | } else { |
||
1507 | $userIds = array( |
||
1508 | $this->userId, |
||
1509 | $this->recoveryKeyId |
||
1510 | ); |
||
1511 | } |
||
1512 | $filteredUids = $this->filterShareReadyUsers($userIds); |
||
1513 | |||
1514 | //decrypt file key |
||
1515 | $encKeyfile = Keymanager::getFileKey($this->view, $this, $file); |
||
1516 | $shareKey = Keymanager::getShareKey($this->view, $this->recoveryKeyId, $this, $file); |
||
1517 | $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); |
||
1518 | // encrypt file key again to all users, this time with the new public key for the recovered use |
||
1519 | $userPubKeys = Keymanager::getPublicKeys($this->view, $filteredUids['ready']); |
||
1520 | $multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys); |
||
1521 | |||
1522 | Keymanager::setFileKey($this->view, $this, $file, $multiEncKey['data']); |
||
1523 | Keymanager::setShareKeys($this->view, $this, $file, $multiEncKey['keys']); |
||
1524 | |||
1525 | } |
||
1526 | |||
1527 | /** |
||
1528 | * collect all files and recover them one by one |
||
1529 | * @param string $path to look for files keys |
||
1530 | * @param string $privateKey private recovery key which is used to decrypt the files |
||
1531 | */ |
||
1532 | private function recoverAllFiles($path, $privateKey) { |
||
1533 | $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); |
||
1534 | foreach ($dirContent as $item) { |
||
1535 | // get relative path from files_encryption/keyfiles |
||
1536 | $filePath = substr($item['path'], strlen('files_encryption/keys')); |
||
1537 | if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { |
||
1538 | $this->recoverAllFiles($filePath . '/', $privateKey); |
||
1539 | } else { |
||
1540 | $this->recoverFile($filePath, $privateKey); |
||
1541 | } |
||
1542 | } |
||
1543 | } |
||
1544 | |||
1545 | /** |
||
1546 | * recover users files in case of password lost |
||
1547 | * @param string $recoveryPassword |
||
1548 | */ |
||
1549 | public function recoverUsersFiles($recoveryPassword) { |
||
1550 | |||
1551 | $encryptedKey = Keymanager::getPrivateSystemKey( $this->recoveryKeyId); |
||
1552 | $privateKey = Crypt::decryptPrivateKey($encryptedKey, $recoveryPassword); |
||
1553 | |||
1554 | $this->recoverAllFiles('/', $privateKey); |
||
1555 | } |
||
1556 | |||
1557 | /** |
||
1558 | * create a backup of all keys from the user |
||
1559 | * |
||
1560 | * @param string $purpose define the purpose of the backup, will be part of the backup folder name |
||
1561 | * @param boolean $timestamp (optional) should a timestamp be added, default true |
||
1562 | * @param boolean $includeUserKeys (optional) include users private-/public-key, default true |
||
1563 | */ |
||
1564 | public function backupAllKeys($purpose, $timestamp = true, $includeUserKeys = true) { |
||
1565 | $this->userId; |
||
1566 | $backupDir = $this->encryptionDir . '/backup.' . $purpose; |
||
1567 | $backupDir .= ($timestamp) ? '.' . date("Y-m-d_H-i-s") . '/' : '/'; |
||
1568 | $this->view->mkdir($backupDir); |
||
1569 | $this->view->copy($this->keysPath, $backupDir . 'keys/'); |
||
1570 | if ($includeUserKeys) { |
||
1571 | $this->view->copy($this->privateKeyPath, $backupDir . $this->userId . '.privateKey'); |
||
1572 | $this->view->copy($this->publicKeyPath, $backupDir . $this->userId . '.publicKey'); |
||
1573 | } |
||
1574 | } |
||
1575 | |||
1576 | /** |
||
1577 | * restore backup |
||
1578 | * |
||
1579 | * @param string $backup complete name of the backup |
||
1580 | * @return boolean |
||
1581 | */ |
||
1582 | public function restoreBackup($backup) { |
||
1583 | $backupDir = $this->encryptionDir . '/backup.' . $backup . '/'; |
||
1584 | |||
1585 | $fileKeysRestored = $this->view->rename($backupDir . 'keys', $this->encryptionDir . '/keys'); |
||
1586 | |||
1587 | $pubKeyRestored = $privKeyRestored = true; |
||
1588 | if ( |
||
1589 | $this->view->file_exists($backupDir . $this->userId . '.privateKey') && |
||
1590 | $this->view->file_exists($backupDir . $this->userId . '.privateKey') |
||
1591 | ) { |
||
1592 | |||
1593 | $pubKeyRestored = $this->view->rename($backupDir . $this->userId . '.publicKey', $this->publicKeyPath); |
||
1594 | $privKeyRestored = $this->view->rename($backupDir . $this->userId . '.privateKey', $this->privateKeyPath); |
||
1595 | } |
||
1596 | |||
1597 | if ($fileKeysRestored && $pubKeyRestored && $privKeyRestored) { |
||
1598 | $this->view->deleteAll($backupDir); |
||
1599 | |||
1600 | return true; |
||
1601 | } |
||
1602 | |||
1603 | return false; |
||
1604 | } |
||
1605 | |||
1606 | /** |
||
1607 | * delete backup |
||
1608 | * |
||
1609 | * @param string $backup complete name of the backup |
||
1610 | * @return boolean |
||
1611 | */ |
||
1612 | public function deleteBackup($backup) { |
||
1613 | $backupDir = $this->encryptionDir . '/backup.' . $backup . '/'; |
||
1614 | return $this->view->deleteAll($backupDir); |
||
1615 | } |
||
1616 | |||
1617 | /** |
||
1618 | * check if the file is stored on a system wide mount point |
||
1619 | * @param string $path relative to /data/user with leading '/' |
||
1620 | * @param string $uid |
||
1621 | * @return boolean |
||
1622 | */ |
||
1623 | public function isSystemWideMountPoint($path, $uid) { |
||
1637 | |||
1638 | /** |
||
1639 | * check if mount point is applicable to user |
||
1640 | * |
||
1641 | * @param array $mount contains $mount['applicable']['users'], $mount['applicable']['groups'] |
||
1642 | * @param string $uid |
||
1643 | * @return boolean |
||
1644 | */ |
||
1645 | protected function isMountPointApplicableToUser($mount, $uid) { |
||
1660 | |||
1661 | /** |
||
1662 | * decrypt private key and add it to the current session |
||
1663 | * @param array $params with 'uid' and 'password' |
||
1664 | * @return mixed session or false |
||
1665 | */ |
||
1666 | public function initEncryption($params) { |
||
1667 | |||
1668 | $session = new Session($this->view); |
||
1669 | |||
1670 | // we tried to initialize the encryption app for this session |
||
1671 | $session->setInitialized(Session::INIT_EXECUTED); |
||
1672 | |||
1673 | $encryptedKey = Keymanager::getPrivateKey($this->view, $params['uid']); |
||
1674 | |||
1675 | $privateKey = false; |
||
1676 | if ($encryptedKey) { |
||
1677 | $privateKey = Crypt::decryptPrivateKey($encryptedKey, $params['password']); |
||
1678 | } |
||
1679 | |||
1680 | if ($privateKey === false) { |
||
1681 | \OCP\Util::writeLog('Encryption library', 'Private key for user "' . $params['uid'] |
||
1682 | . '" is not valid! Maybe the user password was changed from outside if so please change it back to gain access', \OCP\Util::ERROR); |
||
1683 | return false; |
||
1684 | } |
||
1685 | |||
1686 | $session->setPrivateKey($privateKey); |
||
1687 | $session->setInitialized(Session::INIT_SUCCESSFUL); |
||
1688 | |||
1689 | return $session; |
||
1690 | } |
||
1691 | |||
1692 | /* |
||
1693 | * remove encryption related keys from the session |
||
1694 | */ |
||
1695 | public function closeEncryptionSession() { |
||
1696 | $session = new Session($this->view); |
||
1697 | $session->closeSession(); |
||
1698 | } |
||
1699 | |||
1700 | } |
||
1701 |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: