stream.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Robin Appelman
  6. * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, 2011 Robin Appelman
  7. * <icewind1991@gmail.com>
  8. *
  9. * This library is free software; you can redistribute it and/or
  10. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  11. * License as published by the Free Software Foundation; either
  12. * version 3 of the License, or any later version.
  13. *
  14. * This library is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public
  20. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. /**
  24. * transparently encrypted filestream
  25. *
  26. * you can use it as wrapper around an existing stream by setting CryptStream::$sourceStreams['foo']=array('path'=>$path,'stream'=>$stream)
  27. * and then fopen('crypt://streams/foo');
  28. */
  29. namespace OCA\Encryption;
  30. /**
  31. * @brief Provides 'crypt://' stream wrapper protocol.
  32. * @note We use a stream wrapper because it is the most secure way to handle
  33. * decrypted content transfers. There is no safe way to decrypt the entire file
  34. * somewhere on the server, so we have to encrypt and decrypt blocks on the fly.
  35. * @note Paths used with this protocol MUST BE RELATIVE. Use URLs like:
  36. * crypt://filename, or crypt://subdirectory/filename, NOT
  37. * crypt:///home/user/owncloud/data. Otherwise keyfiles will be put in
  38. * [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and
  39. * will not be accessible to other methods.
  40. * @note Data read and written must always be 8192 bytes long, as this is the
  41. * buffer size used internally by PHP. The encryption process makes the input
  42. * data longer, and input is chunked into smaller pieces in order to result in
  43. * a 8192 encrypted block size.
  44. * @note When files are deleted via webdav, or when they are updated and the
  45. * previous version deleted, this is handled by OC\Files\View, and thus the
  46. * encryption proxies are used and keyfiles deleted.
  47. */
  48. class Stream {
  49. private $plainKey;
  50. private $encKeyfiles;
  51. private $rawPath; // The raw path relative to the data dir
  52. private $relPath; // rel path to users file dir
  53. private $userId;
  54. private $handle; // Resource returned by fopen
  55. private $meta = array(); // Header / meta for source stream
  56. private $writeCache;
  57. private $size;
  58. private $unencryptedSize;
  59. private $publicKey;
  60. private $encKeyfile;
  61. /**
  62. * @var \OC\Files\View
  63. */
  64. private $rootView; // a fsview object set to '/'
  65. /**
  66. * @var \OCA\Encryption\Session
  67. */
  68. private $session;
  69. private $privateKey;
  70. /**
  71. * @param $path
  72. * @param $mode
  73. * @param $options
  74. * @param $opened_path
  75. * @return bool
  76. */
  77. public function stream_open($path, $mode, $options, &$opened_path) {
  78. if (!isset($this->rootView)) {
  79. $this->rootView = new \OC_FilesystemView('/');
  80. }
  81. $this->session = new \OCA\Encryption\Session($this->rootView);
  82. $this->privateKey = $this->session->getPrivateKey($this->userId);
  83. $util = new Util($this->rootView, \OCP\USER::getUser());
  84. $this->userId = $util->getUserId();
  85. // Strip identifier text from path, this gives us the path relative to data/<user>/files
  86. $this->relPath = \OC\Files\Filesystem::normalizePath(str_replace('crypt://', '', $path));
  87. // rawPath is relative to the data directory
  88. $this->rawPath = $util->getUserFilesDir() . $this->relPath;
  89. // Disable fileproxies so we can get the file size and open the source file without recursive encryption
  90. $proxyStatus = \OC_FileProxy::$enabled;
  91. \OC_FileProxy::$enabled = false;
  92. if (
  93. $mode === 'w'
  94. or $mode === 'w+'
  95. or $mode === 'wb'
  96. or $mode === 'wb+'
  97. ) {
  98. // We're writing a new file so start write counter with 0 bytes
  99. $this->size = 0;
  100. $this->unencryptedSize = 0;
  101. } else {
  102. if($this->privateKey === false) {
  103. // if private key is not valid redirect user to a error page
  104. \OCA\Encryption\Helper::redirectToErrorPage();
  105. }
  106. $this->size = $this->rootView->filesize($this->rawPath, $mode);
  107. }
  108. $this->handle = $this->rootView->fopen($this->rawPath, $mode);
  109. \OC_FileProxy::$enabled = $proxyStatus;
  110. if (!is_resource($this->handle)) {
  111. \OCP\Util::writeLog('Encryption library', 'failed to open file "' . $this->rawPath . '"', \OCP\Util::ERROR);
  112. } else {
  113. $this->meta = stream_get_meta_data($this->handle);
  114. }
  115. return is_resource($this->handle);
  116. }
  117. /**
  118. * @param $offset
  119. * @param int $whence
  120. */
  121. public function stream_seek($offset, $whence = SEEK_SET) {
  122. $this->flush();
  123. fseek($this->handle, $offset, $whence);
  124. }
  125. /**
  126. * @param $count
  127. * @return bool|string
  128. * @throws \Exception
  129. */
  130. public function stream_read($count) {
  131. $this->writeCache = '';
  132. if ($count !== 8192) {
  133. // $count will always be 8192 https://bugs.php.net/bug.php?id=21641
  134. // This makes this function a lot simpler, but will break this class if the above 'bug' gets 'fixed'
  135. \OCP\Util::writeLog('Encryption library', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL);
  136. die();
  137. }
  138. // Get the data from the file handle
  139. $data = fread($this->handle, 8192);
  140. $result = null;
  141. if (strlen($data)) {
  142. if (!$this->getKey()) {
  143. // Error! We don't have a key to decrypt the file with
  144. throw new \Exception(
  145. 'Encryption key not found for "' . $this->rawPath . '" during attempted read via stream');
  146. } else {
  147. // Decrypt data
  148. $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey);
  149. }
  150. }
  151. return $result;
  152. }
  153. /**
  154. * @brief Encrypt and pad data ready for writing to disk
  155. * @param string $plainData data to be encrypted
  156. * @param string $key key to use for encryption
  157. * @return string encrypted data on success, false on failure
  158. */
  159. public function preWriteEncrypt($plainData, $key) {
  160. // Encrypt data to 'catfile', which includes IV
  161. if ($encrypted = Crypt::symmetricEncryptFileContent($plainData, $key)) {
  162. return $encrypted;
  163. } else {
  164. return false;
  165. }
  166. }
  167. /**
  168. * @brief Fetch the plain encryption key for the file and set it as plainKey property
  169. * @internal param bool $generate if true, a new key will be generated if none can be found
  170. * @return bool true on key found and set, false on key not found and new key generated and set
  171. */
  172. public function getKey() {
  173. // Check if key is already set
  174. if (isset($this->plainKey) && isset($this->encKeyfile)) {
  175. return true;
  176. }
  177. // Fetch and decrypt keyfile
  178. // Fetch existing keyfile
  179. $this->encKeyfile = Keymanager::getFileKey($this->rootView, $this->userId, $this->relPath);
  180. // If a keyfile already exists
  181. if ($this->encKeyfile) {
  182. // if there is no valid private key return false
  183. if ($this->privateKey === false) {
  184. // if private key is not valid redirect user to a error page
  185. \OCA\Encryption\Helper::redirectToErrorPage();
  186. return false;
  187. }
  188. $shareKey = Keymanager::getShareKey($this->rootView, $this->userId, $this->relPath);
  189. $this->plainKey = Crypt::multiKeyDecrypt($this->encKeyfile, $shareKey, $this->privateKey);
  190. return true;
  191. } else {
  192. return false;
  193. }
  194. }
  195. /**
  196. * @brief Handle plain data from the stream, and write it in 8192 byte blocks
  197. * @param string $data data to be written to disk
  198. * @note the data will be written to the path stored in the stream handle, set in stream_open()
  199. * @note $data is only ever be a maximum of 8192 bytes long. This is set by PHP internally. stream_write() is called multiple times in a loop on data larger than 8192 bytes
  200. * @note Because the encryption process used increases the length of $data, a writeCache is used to carry over data which would not fit in the required block size
  201. * @note Padding is added to each encrypted block to ensure that the resulting block is exactly 8192 bytes. This is removed during stream_read
  202. * @note PHP automatically updates the file pointer after writing data to reflect it's length. There is generally no need to update the poitner manually using fseek
  203. */
  204. public function stream_write($data) {
  205. // if there is no valid private key return false
  206. if ($this->privateKey === false) {
  207. $this->size = 0;
  208. return strlen($data);
  209. }
  210. // Disable the file proxies so that encryption is not
  211. // automatically attempted when the file is written to disk -
  212. // we are handling that separately here and we don't want to
  213. // get into an infinite loop
  214. $proxyStatus = \OC_FileProxy::$enabled;
  215. \OC_FileProxy::$enabled = false;
  216. // Get the length of the unencrypted data that we are handling
  217. $length = strlen($data);
  218. // Find out where we are up to in the writing of data to the
  219. // file
  220. $pointer = ftell($this->handle);
  221. // Get / generate the keyfile for the file we're handling
  222. // If we're writing a new file (not overwriting an existing
  223. // one), save the newly generated keyfile
  224. if (!$this->getKey()) {
  225. $this->plainKey = Crypt::generateKey();
  226. }
  227. // If extra data is left over from the last round, make sure it
  228. // is integrated into the next 6126 / 8192 block
  229. if ($this->writeCache) {
  230. // Concat writeCache to start of $data
  231. $data = $this->writeCache . $data;
  232. // Clear the write cache, ready for reuse - it has been
  233. // flushed and its old contents processed
  234. $this->writeCache = '';
  235. }
  236. // While there still remains some data to be processed & written
  237. while (strlen($data) > 0) {
  238. // Remaining length for this iteration, not of the
  239. // entire file (may be greater than 8192 bytes)
  240. $remainingLength = strlen($data);
  241. // If data remaining to be written is less than the
  242. // size of 1 6126 byte block
  243. if ($remainingLength < 6126) {
  244. // Set writeCache to contents of $data
  245. // The writeCache will be carried over to the
  246. // next write round, and added to the start of
  247. // $data to ensure that written blocks are
  248. // always the correct length. If there is still
  249. // data in writeCache after the writing round
  250. // has finished, then the data will be written
  251. // to disk by $this->flush().
  252. $this->writeCache = $data;
  253. // Clear $data ready for next round
  254. $data = '';
  255. } else {
  256. // Read the chunk from the start of $data
  257. $chunk = substr($data, 0, 6126);
  258. $encrypted = $this->preWriteEncrypt($chunk, $this->plainKey);
  259. // Write the data chunk to disk. This will be
  260. // attended to the last data chunk if the file
  261. // being handled totals more than 6126 bytes
  262. fwrite($this->handle, $encrypted);
  263. // Remove the chunk we just processed from
  264. // $data, leaving only unprocessed data in $data
  265. // var, for handling on the next round
  266. $data = substr($data, 6126);
  267. }
  268. }
  269. $this->size = max($this->size, $pointer + $length);
  270. $this->unencryptedSize += $length;
  271. \OC_FileProxy::$enabled = $proxyStatus;
  272. return $length;
  273. }
  274. /**
  275. * @param $option
  276. * @param $arg1
  277. * @param $arg2
  278. */
  279. public function stream_set_option($option, $arg1, $arg2) {
  280. $return = false;
  281. switch ($option) {
  282. case STREAM_OPTION_BLOCKING:
  283. $return = stream_set_blocking($this->handle, $arg1);
  284. break;
  285. case STREAM_OPTION_READ_TIMEOUT:
  286. $return = stream_set_timeout($this->handle, $arg1, $arg2);
  287. break;
  288. case STREAM_OPTION_WRITE_BUFFER:
  289. $return = stream_set_write_buffer($this->handle, $arg1);
  290. }
  291. return $return;
  292. }
  293. /**
  294. * @return array
  295. */
  296. public function stream_stat() {
  297. return fstat($this->handle);
  298. }
  299. /**
  300. * @param $mode
  301. */
  302. public function stream_lock($mode) {
  303. return flock($this->handle, $mode);
  304. }
  305. /**
  306. * @return bool
  307. */
  308. public function stream_flush() {
  309. return fflush($this->handle);
  310. // Not a typo: http://php.net/manual/en/function.fflush.php
  311. }
  312. /**
  313. * @return bool
  314. */
  315. public function stream_eof() {
  316. return feof($this->handle);
  317. }
  318. private function flush() {
  319. if ($this->writeCache) {
  320. // Set keyfile property for file in question
  321. $this->getKey();
  322. $encrypted = $this->preWriteEncrypt($this->writeCache, $this->plainKey);
  323. fwrite($this->handle, $encrypted);
  324. $this->writeCache = '';
  325. }
  326. }
  327. /**
  328. * @return bool
  329. */
  330. public function stream_close() {
  331. $this->flush();
  332. // if there is no valid private key return false
  333. if ($this->privateKey === false) {
  334. // cleanup
  335. if ($this->meta['mode'] !== 'r' && $this->meta['mode'] !== 'rb') {
  336. // Disable encryption proxy to prevent recursive calls
  337. $proxyStatus = \OC_FileProxy::$enabled;
  338. \OC_FileProxy::$enabled = false;
  339. if ($this->rootView->file_exists($this->rawPath) && $this->size === 0) {
  340. $this->rootView->unlink($this->rawPath);
  341. }
  342. // Re-enable proxy - our work is done
  343. \OC_FileProxy::$enabled = $proxyStatus;
  344. }
  345. // if private key is not valid redirect user to a error page
  346. \OCA\Encryption\Helper::redirectToErrorPage();
  347. }
  348. if (
  349. $this->meta['mode'] !== 'r'
  350. and $this->meta['mode'] !== 'rb'
  351. and $this->size > 0
  352. ) {
  353. // Disable encryption proxy to prevent recursive calls
  354. $proxyStatus = \OC_FileProxy::$enabled;
  355. \OC_FileProxy::$enabled = false;
  356. // Fetch user's public key
  357. $this->publicKey = Keymanager::getPublicKey($this->rootView, $this->userId);
  358. // Check if OC sharing api is enabled
  359. $sharingEnabled = \OCP\Share::isEnabled();
  360. $util = new Util($this->rootView, $this->userId);
  361. // Get all users sharing the file includes current user
  362. $uniqueUserIds = $util->getSharingUsersArray($sharingEnabled, $this->relPath, $this->userId);
  363. // Fetch public keys for all sharing users
  364. $publicKeys = Keymanager::getPublicKeys($this->rootView, $uniqueUserIds);
  365. // Encrypt enc key for all sharing users
  366. $this->encKeyfiles = Crypt::multiKeyEncrypt($this->plainKey, $publicKeys);
  367. // Save the new encrypted file key
  368. Keymanager::setFileKey($this->rootView, $this->relPath, $this->userId, $this->encKeyfiles['data']);
  369. // Save the sharekeys
  370. Keymanager::setShareKeys($this->rootView, $this->relPath, $this->encKeyfiles['keys']);
  371. // get file info
  372. $fileInfo = $this->rootView->getFileInfo($this->rawPath);
  373. if (!is_array($fileInfo)) {
  374. $fileInfo = array();
  375. }
  376. // Re-enable proxy - our work is done
  377. \OC_FileProxy::$enabled = $proxyStatus;
  378. // set encryption data
  379. $fileInfo['encrypted'] = true;
  380. $fileInfo['size'] = $this->size;
  381. $fileInfo['unencrypted_size'] = $this->unencryptedSize;
  382. // set fileinfo
  383. $this->rootView->putFileInfo($this->rawPath, $fileInfo);
  384. }
  385. return fclose($this->handle);
  386. }
  387. }