stream.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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. */
  45. class Stream {
  46. public static $sourceStreams = array();
  47. // TODO: make all below properties private again once unit testing is
  48. // configured correctly
  49. public $rawPath; // The raw path received by stream_open
  50. public $path_f; // The raw path formatted to include username and data dir
  51. private $userId;
  52. private $handle; // Resource returned by fopen
  53. private $path;
  54. private $readBuffer; // For streams that dont support seeking
  55. private $meta = array(); // Header / meta for source stream
  56. private $count;
  57. private $writeCache;
  58. public $size;
  59. private $publicKey;
  60. private $keyfile;
  61. private $encKeyfile;
  62. private static $view; // a fsview object set to user dir
  63. private $rootView; // a fsview object set to '/'
  64. public function stream_open( $path, $mode, $options, &$opened_path ) {
  65. // Get access to filesystem via filesystemview object
  66. if ( !self::$view ) {
  67. self::$view = new \OC_FilesystemView( $this->userId . '/' );
  68. }
  69. // Set rootview object if necessary
  70. if ( ! $this->rootView ) {
  71. $this->rootView = new \OC_FilesystemView( $this->userId . '/' );
  72. }
  73. $this->userId = \OCP\User::getUser();
  74. // Get the bare file path
  75. $path = str_replace( 'crypt://', '', $path );
  76. $this->rawPath = $path;
  77. $this->path_f = $this->userId . '/files/' . $path;
  78. if (
  79. dirname( $path ) == 'streams'
  80. and isset( self::$sourceStreams[basename( $path )] )
  81. ) {
  82. // Is this just for unit testing purposes?
  83. $this->handle = self::$sourceStreams[basename( $path )]['stream'];
  84. $this->path = self::$sourceStreams[basename( $path )]['path'];
  85. $this->size = self::$sourceStreams[basename( $path )]['size'];
  86. } else {
  87. if (
  88. $mode == 'w'
  89. or $mode == 'w+'
  90. or $mode == 'wb'
  91. or $mode == 'wb+'
  92. ) {
  93. $this->size = 0;
  94. } else {
  95. $this->size = self::$view->filesize( $this->path_f, $mode );
  96. //$this->size = filesize( $path );
  97. }
  98. // Disable fileproxies so we can open the source file without recursive encryption
  99. \OC_FileProxy::$enabled = false;
  100. //$this->handle = fopen( $path, $mode );
  101. $this->handle = self::$view->fopen( $this->path_f, $mode );
  102. \OC_FileProxy::$enabled = true;
  103. if ( !is_resource( $this->handle ) ) {
  104. \OCP\Util::writeLog( 'files_encryption', 'failed to open '.$path, \OCP\Util::ERROR );
  105. }
  106. }
  107. if ( is_resource( $this->handle ) ) {
  108. $this->meta = stream_get_meta_data( $this->handle );
  109. }
  110. return is_resource( $this->handle );
  111. }
  112. public function stream_seek( $offset, $whence = SEEK_SET ) {
  113. $this->flush();
  114. fseek( $this->handle, $offset, $whence );
  115. }
  116. public function stream_tell() {
  117. return ftell($this->handle);
  118. }
  119. public function stream_read( $count ) {
  120. $this->writeCache = '';
  121. if ( $count != 8192 ) {
  122. // $count will always be 8192 https://bugs.php.net/bug.php?id=21641
  123. // This makes this function a lot simpler, but will break this class if the above 'bug' gets 'fixed'
  124. \OCP\Util::writeLog( 'files_encryption', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL );
  125. die();
  126. }
  127. // $pos = ftell( $this->handle );
  128. //
  129. // Get the data from the file handle
  130. $data = fread( $this->handle, 8192 );
  131. if ( strlen( $data ) ) {
  132. $this->getKey();
  133. $result = Crypt::symmetricDecryptFileContent( $data, $this->keyfile );
  134. } else {
  135. $result = '';
  136. }
  137. // $length = $this->size - $pos;
  138. //
  139. // if ( $length < 8192 ) {
  140. //
  141. // $result = substr( $result, 0, $length );
  142. //
  143. // }
  144. return $result;
  145. }
  146. /**
  147. * @brief Encrypt and pad data ready for writing to disk
  148. * @param string $plainData data to be encrypted
  149. * @param string $key key to use for encryption
  150. * @return encrypted data on success, false on failure
  151. */
  152. public function preWriteEncrypt( $plainData, $key ) {
  153. // Encrypt data to 'catfile', which includes IV
  154. if ( $encrypted = Crypt::symmetricEncryptFileContent( $plainData, $key ) ) {
  155. return $encrypted;
  156. } else {
  157. return false;
  158. }
  159. }
  160. /**
  161. * @brief Get the keyfile for the current file, generate one if necessary
  162. * @param bool $generate if true, a new key will be generated if none can be found
  163. * @return bool true on key found and set, false on key not found and new key generated and set
  164. */
  165. public function getKey() {
  166. // If a keyfile already exists for a file named identically to
  167. // file to be written
  168. if ( self::$view->file_exists( $this->userId . '/'. 'files_encryption' . '/' . 'keyfiles' . '/' . $this->rawPath . '.key' ) ) {
  169. // TODO: add error handling for when file exists but no
  170. // keyfile
  171. // Fetch existing keyfile
  172. $this->encKeyfile = Keymanager::getFileKey( $this->rootView, $this->userId, $this->rawPath );
  173. $this->getUser();
  174. $session = new Session();
  175. $privateKey = $session->getPrivateKey( $this->userId );
  176. $this->keyfile = Crypt::keyDecrypt( $this->encKeyfile, $privateKey );
  177. return true;
  178. } else {
  179. return false;
  180. }
  181. }
  182. public function getuser() {
  183. // Only get the user again if it isn't already set
  184. if ( empty( $this->userId ) ) {
  185. // TODO: Move this user call out of here - it belongs
  186. // elsewhere
  187. $this->userId = \OCP\User::getUser();
  188. }
  189. // TODO: Add a method for getting the user in case OCP\User::
  190. // getUser() doesn't work (can that scenario ever occur?)
  191. }
  192. /**
  193. * @brief Handle plain data from the stream, and write it in 8192 byte blocks
  194. * @param string $data data to be written to disk
  195. * @note the data will be written to the path stored in the stream handle, set in stream_open()
  196. * @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
  197. * @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
  198. * @note Padding is added to each encrypted block to ensure that the resulting block is exactly 8192 bytes. This is removed during stream_read
  199. * @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
  200. */
  201. public function stream_write( $data ) {
  202. // Disable the file proxies so that encryption is not
  203. // automatically attempted when the file is written to disk -
  204. // we are handling that separately here and we don't want to
  205. // get into an infinite loop
  206. \OC_FileProxy::$enabled = false;
  207. // Get the length of the unencrypted data that we are handling
  208. $length = strlen( $data );
  209. // So far this round, no data has been written
  210. $written = 0;
  211. // Find out where we are up to in the writing of data to the
  212. // file
  213. $pointer = ftell( $this->handle );
  214. // Make sure the userId is set
  215. $this->getuser();
  216. // TODO: Check if file is shared, if so, use multiKeyEncrypt and
  217. // save shareKeys in necessary user directories
  218. // Get / generate the keyfile for the file we're handling
  219. // If we're writing a new file (not overwriting an existing
  220. // one), save the newly generated keyfile
  221. if ( ! $this->getKey() ) {
  222. $this->keyfile = Crypt::generateKey();
  223. $this->publicKey = Keymanager::getPublicKey( $this->rootView, $this->userId );
  224. $this->encKeyfile = Crypt::keyEncrypt( $this->keyfile, $this->publicKey );
  225. $view = new \OC_FilesystemView( '/' );
  226. $userId = \OCP\User::getUser();
  227. // Save the new encrypted file key
  228. Keymanager::setFileKey( $view, $this->rawPath, $userId, $this->encKeyfile );
  229. }
  230. // If extra data is left over from the last round, make sure it
  231. // is integrated into the next 6126 / 8192 block
  232. if ( $this->writeCache ) {
  233. // Concat writeCache to start of $data
  234. $data = $this->writeCache . $data;
  235. // Clear the write cache, ready for resuse - it has been
  236. // flushed and its old contents processed
  237. $this->writeCache = '';
  238. }
  239. //
  240. // // Make sure we always start on a block start
  241. if ( 0 != ( $pointer % 8192 ) ) {
  242. // if the current position of
  243. // file indicator is not aligned to a 8192 byte block, fix it
  244. // so that it is
  245. // fseek( $this->handle, - ( $pointer % 8192 ), SEEK_CUR );
  246. //
  247. // $pointer = ftell( $this->handle );
  248. //
  249. // $unencryptedNewBlock = fread( $this->handle, 8192 );
  250. //
  251. // fseek( $this->handle, - ( $currentPos % 8192 ), SEEK_CUR );
  252. //
  253. // $block = Crypt::symmetricDecryptFileContent( $unencryptedNewBlock, $this->keyfile );
  254. //
  255. // $x = substr( $block, 0, $currentPos % 8192 );
  256. //
  257. // $data = $x . $data;
  258. //
  259. // fseek( $this->handle, - ( $currentPos % 8192 ), SEEK_CUR );
  260. //
  261. }
  262. // $currentPos = ftell( $this->handle );
  263. // // While there still remains somed data to be processed & written
  264. while( strlen( $data ) > 0 ) {
  265. //
  266. // // Remaining length for this iteration, not of the
  267. // // entire file (may be greater than 8192 bytes)
  268. // $remainingLength = strlen( $data );
  269. //
  270. // // If data remaining to be written is less than the
  271. // // size of 1 6126 byte block
  272. if ( strlen( $data ) < 6126 ) {
  273. // Set writeCache to contents of $data
  274. // The writeCache will be carried over to the
  275. // next write round, and added to the start of
  276. // $data to ensure that written blocks are
  277. // always the correct length. If there is still
  278. // data in writeCache after the writing round
  279. // has finished, then the data will be written
  280. // to disk by $this->flush().
  281. $this->writeCache = $data;
  282. // Clear $data ready for next round
  283. $data = '';
  284. //
  285. } else {
  286. // Read the chunk from the start of $data
  287. $chunk = substr( $data, 0, 6126 );
  288. $encrypted = $this->preWriteEncrypt( $chunk, $this->keyfile );
  289. // Write the data chunk to disk. This will be
  290. // attended to the last data chunk if the file
  291. // being handled totals more than 6126 bytes
  292. fwrite( $this->handle, $encrypted );
  293. $writtenLen = strlen( $encrypted );
  294. //fseek( $this->handle, $writtenLen, SEEK_CUR );
  295. // Remove the chunk we just processed from
  296. // $data, leaving only unprocessed data in $data
  297. // var, for handling on the next round
  298. $data = substr( $data, 6126 );
  299. }
  300. }
  301. $this->size = max( $this->size, $pointer + $length );
  302. return $length;
  303. }
  304. public function stream_set_option( $option, $arg1, $arg2 ) {
  305. switch($option) {
  306. case STREAM_OPTION_BLOCKING:
  307. stream_set_blocking( $this->handle, $arg1 );
  308. break;
  309. case STREAM_OPTION_READ_TIMEOUT:
  310. stream_set_timeout( $this->handle, $arg1, $arg2 );
  311. break;
  312. case STREAM_OPTION_WRITE_BUFFER:
  313. stream_set_write_buffer( $this->handle, $arg1, $arg2 );
  314. }
  315. }
  316. public function stream_stat() {
  317. return fstat($this->handle);
  318. }
  319. public function stream_lock( $mode ) {
  320. flock( $this->handle, $mode );
  321. }
  322. public function stream_flush() {
  323. return fflush( $this->handle );
  324. // Not a typo: http://php.net/manual/en/function.fflush.php
  325. }
  326. public function stream_eof() {
  327. return feof($this->handle);
  328. }
  329. private function flush() {
  330. if ( $this->writeCache ) {
  331. // Set keyfile property for file in question
  332. $this->getKey();
  333. $encrypted = $this->preWriteEncrypt( $this->writeCache, $this->keyfile );
  334. fwrite( $this->handle, $encrypted );
  335. $this->writeCache = '';
  336. }
  337. }
  338. public function stream_close() {
  339. $this->flush();
  340. if (
  341. $this->meta['mode']!='r'
  342. and $this->meta['mode']!='rb'
  343. ) {
  344. \OC\Files\Filesystem::putFileInfo( $this->path, array( 'encrypted' => true, 'size' => $this->size ), '' );
  345. }
  346. return fclose( $this->handle );
  347. }
  348. }