Encryption.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bjoern Schiessle <bjoern@schiessle.org>
  7. * @author Björn Schießle <bjoern@schiessle.org>
  8. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  9. * @author J0WI <J0WI@users.noreply.github.com>
  10. * @author jknockaert <jasper@knockaert.nl>
  11. * @author Joas Schilling <coding@schilljs.com>
  12. * @author Lukas Reschke <lukas@statuscode.ch>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Piotr M <mrow4a@yahoo.com>
  15. * @author Robin Appelman <robin@icewind.nl>
  16. * @author Roeland Jago Douma <roeland@famdouma.nl>
  17. * @author Thomas Müller <thomas.mueller@tmit.eu>
  18. * @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
  19. * @author Vincent Petry <vincent@nextcloud.com>
  20. *
  21. * @license AGPL-3.0
  22. *
  23. * This code is free software: you can redistribute it and/or modify
  24. * it under the terms of the GNU Affero General Public License, version 3,
  25. * as published by the Free Software Foundation.
  26. *
  27. * This program is distributed in the hope that it will be useful,
  28. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  29. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  30. * GNU Affero General Public License for more details.
  31. *
  32. * You should have received a copy of the GNU Affero General Public License, version 3,
  33. * along with this program. If not, see <http://www.gnu.org/licenses/>
  34. *
  35. */
  36. namespace OC\Files\Storage\Wrapper;
  37. use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
  38. use OC\Encryption\Update;
  39. use OC\Encryption\Util;
  40. use OC\Files\Cache\CacheEntry;
  41. use OC\Files\Filesystem;
  42. use OC\Files\Mount\Manager;
  43. use OC\Files\ObjectStore\ObjectStoreStorage;
  44. use OC\Files\Storage\LocalTempFileTrait;
  45. use OC\Memcache\ArrayCache;
  46. use OCP\Encryption\Exceptions\GenericEncryptionException;
  47. use OCP\Encryption\IFile;
  48. use OCP\Encryption\IManager;
  49. use OCP\Encryption\Keys\IStorage;
  50. use OCP\Files\Cache\ICacheEntry;
  51. use OCP\Files\Mount\IMountPoint;
  52. use OCP\Files\Storage;
  53. use Psr\Log\LoggerInterface;
  54. class Encryption extends Wrapper {
  55. use LocalTempFileTrait;
  56. /** @var string */
  57. private $mountPoint;
  58. /** @var \OC\Encryption\Util */
  59. private $util;
  60. /** @var \OCP\Encryption\IManager */
  61. private $encryptionManager;
  62. private LoggerInterface $logger;
  63. /** @var string */
  64. private $uid;
  65. /** @var array */
  66. protected $unencryptedSize;
  67. /** @var \OCP\Encryption\IFile */
  68. private $fileHelper;
  69. /** @var IMountPoint */
  70. private $mount;
  71. /** @var IStorage */
  72. private $keyStorage;
  73. /** @var Update */
  74. private $update;
  75. /** @var Manager */
  76. private $mountManager;
  77. /** @var array remember for which path we execute the repair step to avoid recursions */
  78. private $fixUnencryptedSizeOf = [];
  79. /** @var ArrayCache */
  80. private $arrayCache;
  81. /**
  82. * @param array $parameters
  83. */
  84. public function __construct(
  85. $parameters,
  86. IManager $encryptionManager = null,
  87. Util $util = null,
  88. LoggerInterface $logger = null,
  89. IFile $fileHelper = null,
  90. $uid = null,
  91. IStorage $keyStorage = null,
  92. Update $update = null,
  93. Manager $mountManager = null,
  94. ArrayCache $arrayCache = null
  95. ) {
  96. $this->mountPoint = $parameters['mountPoint'];
  97. $this->mount = $parameters['mount'];
  98. $this->encryptionManager = $encryptionManager;
  99. $this->util = $util;
  100. $this->logger = $logger;
  101. $this->uid = $uid;
  102. $this->fileHelper = $fileHelper;
  103. $this->keyStorage = $keyStorage;
  104. $this->unencryptedSize = [];
  105. $this->update = $update;
  106. $this->mountManager = $mountManager;
  107. $this->arrayCache = $arrayCache;
  108. parent::__construct($parameters);
  109. }
  110. /**
  111. * see https://www.php.net/manual/en/function.filesize.php
  112. * The result for filesize when called on a folder is required to be 0
  113. *
  114. * @param string $path
  115. * @return int
  116. */
  117. public function filesize($path) {
  118. $fullPath = $this->getFullPath($path);
  119. /** @var CacheEntry $info */
  120. $info = $this->getCache()->get($path);
  121. if (isset($this->unencryptedSize[$fullPath])) {
  122. $size = $this->unencryptedSize[$fullPath];
  123. // update file cache
  124. if ($info instanceof ICacheEntry) {
  125. $info['encrypted'] = $info['encryptedVersion'];
  126. } else {
  127. if (!is_array($info)) {
  128. $info = [];
  129. }
  130. $info['encrypted'] = true;
  131. $info = new CacheEntry($info);
  132. }
  133. if ($size !== $info->getUnencryptedSize()) {
  134. $this->getCache()->update($info->getId(), [
  135. 'unencrypted_size' => $size
  136. ]);
  137. }
  138. return $size;
  139. }
  140. if (isset($info['fileid']) && $info['encrypted']) {
  141. return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
  142. }
  143. return $this->storage->filesize($path);
  144. }
  145. /**
  146. * @param string $path
  147. * @param array $data
  148. * @return array
  149. */
  150. private function modifyMetaData(string $path, array $data): array {
  151. $fullPath = $this->getFullPath($path);
  152. $info = $this->getCache()->get($path);
  153. if (isset($this->unencryptedSize[$fullPath])) {
  154. $data['encrypted'] = true;
  155. $data['size'] = $this->unencryptedSize[$fullPath];
  156. } else {
  157. if (isset($info['fileid']) && $info['encrypted']) {
  158. $data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
  159. $data['encrypted'] = true;
  160. }
  161. }
  162. if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
  163. $data['encryptedVersion'] = $info['encryptedVersion'];
  164. }
  165. return $data;
  166. }
  167. public function getMetaData($path) {
  168. $data = $this->storage->getMetaData($path);
  169. if (is_null($data)) {
  170. return null;
  171. }
  172. return $this->modifyMetaData($path, $data);
  173. }
  174. public function getDirectoryContent($directory): \Traversable {
  175. $parent = rtrim($directory, '/');
  176. foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
  177. yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
  178. }
  179. }
  180. /**
  181. * see https://www.php.net/manual/en/function.file_get_contents.php
  182. *
  183. * @param string $path
  184. * @return string
  185. */
  186. public function file_get_contents($path) {
  187. $encryptionModule = $this->getEncryptionModule($path);
  188. if ($encryptionModule) {
  189. $handle = $this->fopen($path, "r");
  190. if (!$handle) {
  191. return false;
  192. }
  193. $data = stream_get_contents($handle);
  194. fclose($handle);
  195. return $data;
  196. }
  197. return $this->storage->file_get_contents($path);
  198. }
  199. /**
  200. * see https://www.php.net/manual/en/function.file_put_contents.php
  201. *
  202. * @param string $path
  203. * @param mixed $data
  204. * @return int|false
  205. */
  206. public function file_put_contents($path, $data) {
  207. // file put content will always be translated to a stream write
  208. $handle = $this->fopen($path, 'w');
  209. if (is_resource($handle)) {
  210. $written = fwrite($handle, $data);
  211. fclose($handle);
  212. return $written;
  213. }
  214. return false;
  215. }
  216. /**
  217. * see https://www.php.net/manual/en/function.unlink.php
  218. *
  219. * @param string $path
  220. * @return bool
  221. */
  222. public function unlink($path) {
  223. $fullPath = $this->getFullPath($path);
  224. if ($this->util->isExcluded($fullPath)) {
  225. return $this->storage->unlink($path);
  226. }
  227. $encryptionModule = $this->getEncryptionModule($path);
  228. if ($encryptionModule) {
  229. $this->keyStorage->deleteAllFileKeys($fullPath);
  230. }
  231. return $this->storage->unlink($path);
  232. }
  233. /**
  234. * see https://www.php.net/manual/en/function.rename.php
  235. *
  236. * @param string $source
  237. * @param string $target
  238. * @return bool
  239. */
  240. public function rename($source, $target) {
  241. $result = $this->storage->rename($source, $target);
  242. if ($result &&
  243. // versions always use the keys from the original file, so we can skip
  244. // this step for versions
  245. $this->isVersion($target) === false &&
  246. $this->encryptionManager->isEnabled()) {
  247. $sourcePath = $this->getFullPath($source);
  248. if (!$this->util->isExcluded($sourcePath)) {
  249. $targetPath = $this->getFullPath($target);
  250. if (isset($this->unencryptedSize[$sourcePath])) {
  251. $this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
  252. }
  253. $this->keyStorage->renameKeys($sourcePath, $targetPath);
  254. $module = $this->getEncryptionModule($target);
  255. if ($module) {
  256. $module->update($targetPath, $this->uid, []);
  257. }
  258. }
  259. }
  260. return $result;
  261. }
  262. /**
  263. * see https://www.php.net/manual/en/function.rmdir.php
  264. *
  265. * @param string $path
  266. * @return bool
  267. */
  268. public function rmdir($path) {
  269. $result = $this->storage->rmdir($path);
  270. $fullPath = $this->getFullPath($path);
  271. if ($result &&
  272. $this->util->isExcluded($fullPath) === false &&
  273. $this->encryptionManager->isEnabled()
  274. ) {
  275. $this->keyStorage->deleteAllFileKeys($fullPath);
  276. }
  277. return $result;
  278. }
  279. /**
  280. * check if a file can be read
  281. *
  282. * @param string $path
  283. * @return bool
  284. */
  285. public function isReadable($path) {
  286. $isReadable = true;
  287. $metaData = $this->getMetaData($path);
  288. if (
  289. !$this->is_dir($path) &&
  290. isset($metaData['encrypted']) &&
  291. $metaData['encrypted'] === true
  292. ) {
  293. $fullPath = $this->getFullPath($path);
  294. $module = $this->getEncryptionModule($path);
  295. $isReadable = $module->isReadable($fullPath, $this->uid);
  296. }
  297. return $this->storage->isReadable($path) && $isReadable;
  298. }
  299. /**
  300. * see https://www.php.net/manual/en/function.copy.php
  301. *
  302. * @param string $source
  303. * @param string $target
  304. */
  305. public function copy($source, $target): bool {
  306. $sourcePath = $this->getFullPath($source);
  307. if ($this->util->isExcluded($sourcePath)) {
  308. return $this->storage->copy($source, $target);
  309. }
  310. // need to stream copy file by file in case we copy between a encrypted
  311. // and a unencrypted storage
  312. $this->unlink($target);
  313. return $this->copyFromStorage($this, $source, $target);
  314. }
  315. /**
  316. * see https://www.php.net/manual/en/function.fopen.php
  317. *
  318. * @param string $path
  319. * @param string $mode
  320. * @return resource|bool
  321. * @throws GenericEncryptionException
  322. * @throws ModuleDoesNotExistsException
  323. */
  324. public function fopen($path, $mode) {
  325. // check if the file is stored in the array cache, this means that we
  326. // copy a file over to the versions folder, in this case we don't want to
  327. // decrypt it
  328. if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
  329. $this->arrayCache->remove('encryption_copy_version_' . $path);
  330. return $this->storage->fopen($path, $mode);
  331. }
  332. $encryptionEnabled = $this->encryptionManager->isEnabled();
  333. $shouldEncrypt = false;
  334. $encryptionModule = null;
  335. $header = $this->getHeader($path);
  336. $signed = isset($header['signed']) && $header['signed'] === 'true';
  337. $fullPath = $this->getFullPath($path);
  338. $encryptionModuleId = $this->util->getEncryptionModuleId($header);
  339. if ($this->util->isExcluded($fullPath) === false) {
  340. $size = $unencryptedSize = 0;
  341. $realFile = $this->util->stripPartialFileExtension($path);
  342. $targetExists = $this->is_file($realFile) || $this->file_exists($path);
  343. $targetIsEncrypted = false;
  344. if ($targetExists) {
  345. // in case the file exists we require the explicit module as
  346. // specified in the file header - otherwise we need to fail hard to
  347. // prevent data loss on client side
  348. if (!empty($encryptionModuleId)) {
  349. $targetIsEncrypted = true;
  350. $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
  351. }
  352. if ($this->file_exists($path)) {
  353. $size = $this->storage->filesize($path);
  354. $unencryptedSize = $this->filesize($path);
  355. } else {
  356. $size = $unencryptedSize = 0;
  357. }
  358. }
  359. try {
  360. if (
  361. $mode === 'w'
  362. || $mode === 'w+'
  363. || $mode === 'wb'
  364. || $mode === 'wb+'
  365. ) {
  366. // if we update a encrypted file with a un-encrypted one we change the db flag
  367. if ($targetIsEncrypted && $encryptionEnabled === false) {
  368. $cache = $this->storage->getCache();
  369. if ($cache) {
  370. $entry = $cache->get($path);
  371. $cache->update($entry->getId(), ['encrypted' => 0]);
  372. }
  373. }
  374. if ($encryptionEnabled) {
  375. // if $encryptionModuleId is empty, the default module will be used
  376. $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
  377. $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
  378. $signed = true;
  379. }
  380. } else {
  381. $info = $this->getCache()->get($path);
  382. // only get encryption module if we found one in the header
  383. // or if file should be encrypted according to the file cache
  384. if (!empty($encryptionModuleId)) {
  385. $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
  386. $shouldEncrypt = true;
  387. } elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
  388. // we come from a old installation. No header and/or no module defined
  389. // but the file is encrypted. In this case we need to use the
  390. // OC_DEFAULT_MODULE to read the file
  391. $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
  392. $shouldEncrypt = true;
  393. $targetIsEncrypted = true;
  394. }
  395. }
  396. } catch (ModuleDoesNotExistsException $e) {
  397. $this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
  398. 'exception' => $e,
  399. 'app' => 'core',
  400. ]);
  401. }
  402. // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
  403. if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
  404. if (!$targetExists || !$targetIsEncrypted) {
  405. $shouldEncrypt = false;
  406. }
  407. }
  408. if ($shouldEncrypt === true && $encryptionModule !== null) {
  409. $headerSize = $this->getHeaderSize($path);
  410. $source = $this->storage->fopen($path, $mode);
  411. if (!is_resource($source)) {
  412. return false;
  413. }
  414. $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
  415. $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
  416. $size, $unencryptedSize, $headerSize, $signed);
  417. return $handle;
  418. }
  419. }
  420. return $this->storage->fopen($path, $mode);
  421. }
  422. /**
  423. * perform some plausibility checks if the the unencrypted size is correct.
  424. * If not, we calculate the correct unencrypted size and return it
  425. *
  426. * @param string $path internal path relative to the storage root
  427. * @param int $unencryptedSize size of the unencrypted file
  428. *
  429. * @return int unencrypted size
  430. */
  431. protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
  432. $size = $this->storage->filesize($path);
  433. $result = $unencryptedSize;
  434. if ($unencryptedSize < 0 ||
  435. ($size > 0 && $unencryptedSize === $size)
  436. ) {
  437. // check if we already calculate the unencrypted size for the
  438. // given path to avoid recursions
  439. if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
  440. $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
  441. try {
  442. $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
  443. } catch (\Exception $e) {
  444. $this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
  445. }
  446. unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
  447. }
  448. }
  449. return $result;
  450. }
  451. /**
  452. * calculate the unencrypted size
  453. *
  454. * @param string $path internal path relative to the storage root
  455. * @param int $size size of the physical file
  456. * @param int $unencryptedSize size of the unencrypted file
  457. *
  458. * @return int calculated unencrypted size
  459. */
  460. protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
  461. $headerSize = $this->getHeaderSize($path);
  462. $header = $this->getHeader($path);
  463. $encryptionModule = $this->getEncryptionModule($path);
  464. $stream = $this->storage->fopen($path, 'r');
  465. // if we couldn't open the file we return the old unencrypted size
  466. if (!is_resource($stream)) {
  467. $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
  468. return $unencryptedSize;
  469. }
  470. $newUnencryptedSize = 0;
  471. $size -= $headerSize;
  472. $blockSize = $this->util->getBlockSize();
  473. // if a header exists we skip it
  474. if ($headerSize > 0) {
  475. $this->fread_block($stream, $headerSize);
  476. }
  477. // fast path, else the calculation for $lastChunkNr is bogus
  478. if ($size === 0) {
  479. return 0;
  480. }
  481. $signed = isset($header['signed']) && $header['signed'] === 'true';
  482. $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
  483. // calculate last chunk nr
  484. // next highest is end of chunks, one subtracted is last one
  485. // we have to read the last chunk, we can't just calculate it (because of padding etc)
  486. $lastChunkNr = ceil($size / $blockSize) - 1;
  487. // calculate last chunk position
  488. $lastChunkPos = ($lastChunkNr * $blockSize);
  489. // try to fseek to the last chunk, if it fails we have to read the whole file
  490. if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
  491. $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
  492. }
  493. $lastChunkContentEncrypted = '';
  494. $count = $blockSize;
  495. while ($count > 0) {
  496. $data = $this->fread_block($stream, $blockSize);
  497. $count = strlen($data);
  498. $lastChunkContentEncrypted .= $data;
  499. if (strlen($lastChunkContentEncrypted) > $blockSize) {
  500. $newUnencryptedSize += $unencryptedBlockSize;
  501. $lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
  502. }
  503. }
  504. fclose($stream);
  505. // we have to decrypt the last chunk to get it actual size
  506. $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
  507. $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
  508. $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
  509. // calc the real file size with the size of the last chunk
  510. $newUnencryptedSize += strlen($decryptedLastChunk);
  511. $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
  512. // write to cache if applicable
  513. $cache = $this->storage->getCache();
  514. if ($cache) {
  515. $entry = $cache->get($path);
  516. $cache->update($entry['fileid'], [
  517. 'unencrypted_size' => $newUnencryptedSize
  518. ]);
  519. }
  520. return $newUnencryptedSize;
  521. }
  522. /**
  523. * fread_block
  524. *
  525. * This function is a wrapper around the fread function. It is based on the
  526. * stream_read_block function from lib/private/Files/Streams/Encryption.php
  527. * It calls stream read until the requested $blockSize was received or no remaining data is present.
  528. * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
  529. * remote storage over the internet and it does not care about the given $blockSize.
  530. *
  531. * @param handle the stream to read from
  532. * @param int $blockSize Length of requested data block in bytes
  533. * @return string Data fetched from stream.
  534. */
  535. private function fread_block($handle, int $blockSize): string {
  536. $remaining = $blockSize;
  537. $data = '';
  538. do {
  539. $chunk = fread($handle, $remaining);
  540. $chunk_len = strlen($chunk);
  541. $data .= $chunk;
  542. $remaining -= $chunk_len;
  543. } while (($remaining > 0) && ($chunk_len > 0));
  544. return $data;
  545. }
  546. /**
  547. * @param Storage\IStorage $sourceStorage
  548. * @param string $sourceInternalPath
  549. * @param string $targetInternalPath
  550. * @param bool $preserveMtime
  551. * @return bool
  552. */
  553. public function moveFromStorage(
  554. Storage\IStorage $sourceStorage,
  555. $sourceInternalPath,
  556. $targetInternalPath,
  557. $preserveMtime = true
  558. ) {
  559. if ($sourceStorage === $this) {
  560. return $this->rename($sourceInternalPath, $targetInternalPath);
  561. }
  562. // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
  563. // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
  564. // - copy the file cache update from $this->copyBetweenStorage to this method
  565. // - copy the copyKeys() call from $this->copyBetweenStorage to this method
  566. // - remove $this->copyBetweenStorage
  567. if (!$sourceStorage->isDeletable($sourceInternalPath)) {
  568. return false;
  569. }
  570. $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
  571. if ($result) {
  572. if ($sourceStorage->is_dir($sourceInternalPath)) {
  573. $result &= $sourceStorage->rmdir($sourceInternalPath);
  574. } else {
  575. $result &= $sourceStorage->unlink($sourceInternalPath);
  576. }
  577. }
  578. return $result;
  579. }
  580. /**
  581. * @param Storage\IStorage $sourceStorage
  582. * @param string $sourceInternalPath
  583. * @param string $targetInternalPath
  584. * @param bool $preserveMtime
  585. * @param bool $isRename
  586. * @return bool
  587. */
  588. public function copyFromStorage(
  589. Storage\IStorage $sourceStorage,
  590. $sourceInternalPath,
  591. $targetInternalPath,
  592. $preserveMtime = false,
  593. $isRename = false
  594. ) {
  595. // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
  596. // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
  597. // - copy the file cache update from $this->copyBetweenStorage to this method
  598. // - copy the copyKeys() call from $this->copyBetweenStorage to this method
  599. // - remove $this->copyBetweenStorage
  600. return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
  601. }
  602. /**
  603. * Update the encrypted cache version in the database
  604. *
  605. * @param Storage\IStorage $sourceStorage
  606. * @param string $sourceInternalPath
  607. * @param string $targetInternalPath
  608. * @param bool $isRename
  609. * @param bool $keepEncryptionVersion
  610. */
  611. private function updateEncryptedVersion(
  612. Storage\IStorage $sourceStorage,
  613. $sourceInternalPath,
  614. $targetInternalPath,
  615. $isRename,
  616. $keepEncryptionVersion
  617. ) {
  618. $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
  619. $cacheInformation = [
  620. 'encrypted' => $isEncrypted,
  621. ];
  622. if ($isEncrypted) {
  623. $sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
  624. $targetCacheEntry = $this->getCache()->get($targetInternalPath);
  625. // Rename of the cache already happened, so we do the cleanup on the target
  626. if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
  627. $encryptedVersion = $targetCacheEntry['encryptedVersion'];
  628. $isRename = false;
  629. } else {
  630. $encryptedVersion = $sourceCacheEntry['encryptedVersion'];
  631. }
  632. // In case of a move operation from an unencrypted to an encrypted
  633. // storage the old encrypted version would stay with "0" while the
  634. // correct value would be "1". Thus we manually set the value to "1"
  635. // for those cases.
  636. // See also https://github.com/owncloud/core/issues/23078
  637. if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
  638. $encryptedVersion = 1;
  639. }
  640. $cacheInformation['encryptedVersion'] = $encryptedVersion;
  641. }
  642. // in case of a rename we need to manipulate the source cache because
  643. // this information will be kept for the new target
  644. if ($isRename) {
  645. $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
  646. } else {
  647. $this->getCache()->put($targetInternalPath, $cacheInformation);
  648. }
  649. }
  650. /**
  651. * copy file between two storages
  652. *
  653. * @param Storage\IStorage $sourceStorage
  654. * @param string $sourceInternalPath
  655. * @param string $targetInternalPath
  656. * @param bool $preserveMtime
  657. * @param bool $isRename
  658. * @return bool
  659. * @throws \Exception
  660. */
  661. private function copyBetweenStorage(
  662. Storage\IStorage $sourceStorage,
  663. $sourceInternalPath,
  664. $targetInternalPath,
  665. $preserveMtime,
  666. $isRename
  667. ) {
  668. // for versions we have nothing to do, because versions should always use the
  669. // key from the original file. Just create a 1:1 copy and done
  670. if ($this->isVersion($targetInternalPath) ||
  671. $this->isVersion($sourceInternalPath)) {
  672. // remember that we try to create a version so that we can detect it during
  673. // fopen($sourceInternalPath) and by-pass the encryption in order to
  674. // create a 1:1 copy of the file
  675. $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
  676. $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  677. $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
  678. if ($result) {
  679. $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
  680. // make sure that we update the unencrypted size for the version
  681. if (isset($info['encrypted']) && $info['encrypted'] === true) {
  682. $this->updateUnencryptedSize(
  683. $this->getFullPath($targetInternalPath),
  684. $info->getUnencryptedSize()
  685. );
  686. }
  687. $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
  688. }
  689. return $result;
  690. }
  691. // first copy the keys that we reuse the existing file key on the target location
  692. // and don't create a new one which would break versions for example.
  693. $mount = $this->mountManager->findByStorageId($sourceStorage->getId());
  694. if (count($mount) === 1) {
  695. $mountPoint = $mount[0]->getMountPoint();
  696. $source = $mountPoint . '/' . $sourceInternalPath;
  697. $target = $this->getFullPath($targetInternalPath);
  698. $this->copyKeys($source, $target);
  699. } else {
  700. $this->logger->error('Could not find mount point, can\'t keep encryption keys');
  701. }
  702. if ($sourceStorage->is_dir($sourceInternalPath)) {
  703. $dh = $sourceStorage->opendir($sourceInternalPath);
  704. if (!$this->is_dir($targetInternalPath)) {
  705. $result = $this->mkdir($targetInternalPath);
  706. } else {
  707. $result = true;
  708. }
  709. if (is_resource($dh)) {
  710. while ($result and ($file = readdir($dh)) !== false) {
  711. if (!Filesystem::isIgnoredDir($file)) {
  712. $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
  713. }
  714. }
  715. }
  716. } else {
  717. try {
  718. $source = $sourceStorage->fopen($sourceInternalPath, 'r');
  719. $target = $this->fopen($targetInternalPath, 'w');
  720. [, $result] = \OC_Helper::streamCopy($source, $target);
  721. fclose($source);
  722. fclose($target);
  723. } catch (\Exception $e) {
  724. if (is_resource($source)) {
  725. fclose($source);
  726. }
  727. if (is_resource($target)) {
  728. fclose($target);
  729. }
  730. throw $e;
  731. }
  732. if ($result) {
  733. if ($preserveMtime) {
  734. $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
  735. }
  736. $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
  737. } else {
  738. // delete partially written target file
  739. $this->unlink($targetInternalPath);
  740. // delete cache entry that was created by fopen
  741. $this->getCache()->remove($targetInternalPath);
  742. }
  743. }
  744. return (bool)$result;
  745. }
  746. public function getLocalFile($path) {
  747. if ($this->encryptionManager->isEnabled()) {
  748. $cachedFile = $this->getCachedFile($path);
  749. if (is_string($cachedFile)) {
  750. return $cachedFile;
  751. }
  752. }
  753. return $this->storage->getLocalFile($path);
  754. }
  755. public function isLocal() {
  756. if ($this->encryptionManager->isEnabled()) {
  757. return false;
  758. }
  759. return $this->storage->isLocal();
  760. }
  761. public function stat($path) {
  762. $stat = $this->storage->stat($path);
  763. if (!$stat) {
  764. return false;
  765. }
  766. $fileSize = $this->filesize($path);
  767. $stat['size'] = $fileSize;
  768. $stat[7] = $fileSize;
  769. $stat['hasHeader'] = $this->getHeaderSize($path) > 0;
  770. return $stat;
  771. }
  772. public function hash($type, $path, $raw = false) {
  773. $fh = $this->fopen($path, 'rb');
  774. $ctx = hash_init($type);
  775. hash_update_stream($ctx, $fh);
  776. fclose($fh);
  777. return hash_final($ctx, $raw);
  778. }
  779. /**
  780. * return full path, including mount point
  781. *
  782. * @param string $path relative to mount point
  783. * @return string full path including mount point
  784. */
  785. protected function getFullPath($path) {
  786. return Filesystem::normalizePath($this->mountPoint . '/' . $path);
  787. }
  788. /**
  789. * read first block of encrypted file, typically this will contain the
  790. * encryption header
  791. *
  792. * @param string $path
  793. * @return string
  794. */
  795. protected function readFirstBlock($path) {
  796. $firstBlock = '';
  797. if ($this->storage->is_file($path)) {
  798. $handle = $this->storage->fopen($path, 'r');
  799. $firstBlock = fread($handle, $this->util->getHeaderSize());
  800. fclose($handle);
  801. }
  802. return $firstBlock;
  803. }
  804. /**
  805. * return header size of given file
  806. *
  807. * @param string $path
  808. * @return int
  809. */
  810. protected function getHeaderSize($path) {
  811. $headerSize = 0;
  812. $realFile = $this->util->stripPartialFileExtension($path);
  813. if ($this->storage->is_file($realFile)) {
  814. $path = $realFile;
  815. }
  816. $firstBlock = $this->readFirstBlock($path);
  817. if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
  818. $headerSize = $this->util->getHeaderSize();
  819. }
  820. return $headerSize;
  821. }
  822. /**
  823. * parse raw header to array
  824. *
  825. * @param string $rawHeader
  826. * @return array
  827. */
  828. protected function parseRawHeader($rawHeader) {
  829. $result = [];
  830. if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
  831. $header = $rawHeader;
  832. $endAt = strpos($header, Util::HEADER_END);
  833. if ($endAt !== false) {
  834. $header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
  835. // +1 to not start with an ':' which would result in empty element at the beginning
  836. $exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
  837. $element = array_shift($exploded);
  838. while ($element !== Util::HEADER_END) {
  839. $result[$element] = array_shift($exploded);
  840. $element = array_shift($exploded);
  841. }
  842. }
  843. }
  844. return $result;
  845. }
  846. /**
  847. * read header from file
  848. *
  849. * @param string $path
  850. * @return array
  851. */
  852. protected function getHeader($path) {
  853. $realFile = $this->util->stripPartialFileExtension($path);
  854. $exists = $this->storage->is_file($realFile);
  855. if ($exists) {
  856. $path = $realFile;
  857. }
  858. $result = [];
  859. // first check if it is an encrypted file at all
  860. // We would do query to filecache only if we know that entry in filecache exists
  861. $info = $this->getCache()->get($path);
  862. if (isset($info['encrypted']) && $info['encrypted'] === true) {
  863. $firstBlock = $this->readFirstBlock($path);
  864. $result = $this->parseRawHeader($firstBlock);
  865. // if the header doesn't contain a encryption module we check if it is a
  866. // legacy file. If true, we add the default encryption module
  867. if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
  868. $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
  869. }
  870. }
  871. return $result;
  872. }
  873. /**
  874. * read encryption module needed to read/write the file located at $path
  875. *
  876. * @param string $path
  877. * @return null|\OCP\Encryption\IEncryptionModule
  878. * @throws ModuleDoesNotExistsException
  879. * @throws \Exception
  880. */
  881. protected function getEncryptionModule($path) {
  882. $encryptionModule = null;
  883. $header = $this->getHeader($path);
  884. $encryptionModuleId = $this->util->getEncryptionModuleId($header);
  885. if (!empty($encryptionModuleId)) {
  886. try {
  887. $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
  888. } catch (ModuleDoesNotExistsException $e) {
  889. $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
  890. throw $e;
  891. }
  892. }
  893. return $encryptionModule;
  894. }
  895. /**
  896. * @param string $path
  897. * @param int $unencryptedSize
  898. */
  899. public function updateUnencryptedSize($path, $unencryptedSize) {
  900. $this->unencryptedSize[$path] = $unencryptedSize;
  901. }
  902. /**
  903. * copy keys to new location
  904. *
  905. * @param string $source path relative to data/
  906. * @param string $target path relative to data/
  907. * @return bool
  908. */
  909. protected function copyKeys($source, $target) {
  910. if (!$this->util->isExcluded($source)) {
  911. return $this->keyStorage->copyKeys($source, $target);
  912. }
  913. return false;
  914. }
  915. /**
  916. * check if path points to a files version
  917. *
  918. * @param $path
  919. * @return bool
  920. */
  921. protected function isVersion($path) {
  922. $normalized = Filesystem::normalizePath($path);
  923. return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
  924. }
  925. /**
  926. * check if the given storage should be encrypted or not
  927. *
  928. * @param $path
  929. * @return bool
  930. */
  931. protected function shouldEncrypt($path) {
  932. $fullPath = $this->getFullPath($path);
  933. $mountPointConfig = $this->mount->getOption('encrypt', true);
  934. if ($mountPointConfig === false) {
  935. return false;
  936. }
  937. try {
  938. $encryptionModule = $this->getEncryptionModule($fullPath);
  939. } catch (ModuleDoesNotExistsException $e) {
  940. return false;
  941. }
  942. if ($encryptionModule === null) {
  943. $encryptionModule = $this->encryptionManager->getEncryptionModule();
  944. }
  945. return $encryptionModule->shouldEncrypt($fullPath);
  946. }
  947. public function writeStream(string $path, $stream, int $size = null): int {
  948. // always fall back to fopen
  949. $target = $this->fopen($path, 'w');
  950. [$count, $result] = \OC_Helper::streamCopy($stream, $target);
  951. fclose($stream);
  952. fclose($target);
  953. // object store, stores the size after write and doesn't update this during scan
  954. // manually store the unencrypted size
  955. if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
  956. $this->getCache()->put($path, ['unencrypted_size' => $count]);
  957. }
  958. return $count;
  959. }
  960. }