You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

501 lines
14 KiB

10 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Björn Schießle <bjoern@schiessle.org>
  6. * @author jknockaert <jasper@knockaert.nl>
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. * @author Roeland Jago Douma <roeland@famdouma.nl>
  9. * @author Thomas Müller <thomas.mueller@tmit.eu>
  10. * @author Vincent Petry <pvince81@owncloud.com>
  11. *
  12. * @license AGPL-3.0
  13. *
  14. * This code is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License, version 3,
  16. * as published by the Free Software Foundation.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License, version 3,
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>
  25. *
  26. */
  27. namespace OC\Files\Stream;
  28. use Icewind\Streams\Wrapper;
  29. use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;
  30. class Encryption extends Wrapper {
  31. /** @var \OC\Encryption\Util */
  32. protected $util;
  33. /** @var \OC\Encryption\File */
  34. protected $file;
  35. /** @var \OCP\Encryption\IEncryptionModule */
  36. protected $encryptionModule;
  37. /** @var \OC\Files\Storage\Storage */
  38. protected $storage;
  39. /** @var \OC\Files\Storage\Wrapper\Encryption */
  40. protected $encryptionStorage;
  41. /** @var string */
  42. protected $internalPath;
  43. /** @var string */
  44. protected $cache;
  45. /** @var integer */
  46. protected $size;
  47. /** @var integer */
  48. protected $position;
  49. /** @var integer */
  50. protected $unencryptedSize;
  51. /** @var integer */
  52. protected $headerSize;
  53. /** @var integer */
  54. protected $unencryptedBlockSize;
  55. /** @var array */
  56. protected $header;
  57. /** @var string */
  58. protected $fullPath;
  59. /** @var bool */
  60. protected $signed;
  61. /**
  62. * header data returned by the encryption module, will be written to the file
  63. * in case of a write operation
  64. *
  65. * @var array
  66. */
  67. protected $newHeader;
  68. /**
  69. * user who perform the read/write operation null for public access
  70. *
  71. * @var string
  72. */
  73. protected $uid;
  74. /** @var bool */
  75. protected $readOnly;
  76. /** @var bool */
  77. protected $writeFlag;
  78. /** @var array */
  79. protected $expectedContextProperties;
  80. public function __construct() {
  81. $this->expectedContextProperties = array(
  82. 'source',
  83. 'storage',
  84. 'internalPath',
  85. 'fullPath',
  86. 'encryptionModule',
  87. 'header',
  88. 'uid',
  89. 'file',
  90. 'util',
  91. 'size',
  92. 'unencryptedSize',
  93. 'encryptionStorage',
  94. 'headerSize',
  95. 'signed'
  96. );
  97. }
  98. /**
  99. * Wraps a stream with the provided callbacks
  100. *
  101. * @param resource $source
  102. * @param string $internalPath relative to mount point
  103. * @param string $fullPath relative to data/
  104. * @param array $header
  105. * @param string $uid
  106. * @param \OCP\Encryption\IEncryptionModule $encryptionModule
  107. * @param \OC\Files\Storage\Storage $storage
  108. * @param \OC\Files\Storage\Wrapper\Encryption $encStorage
  109. * @param \OC\Encryption\Util $util
  110. * @param \OC\Encryption\File $file
  111. * @param string $mode
  112. * @param int $size
  113. * @param int $unencryptedSize
  114. * @param int $headerSize
  115. * @param bool $signed
  116. * @param string $wrapper stream wrapper class
  117. * @return resource
  118. *
  119. * @throws \BadMethodCallException
  120. */
  121. public static function wrap($source, $internalPath, $fullPath, array $header,
  122. $uid,
  123. \OCP\Encryption\IEncryptionModule $encryptionModule,
  124. \OC\Files\Storage\Storage $storage,
  125. \OC\Files\Storage\Wrapper\Encryption $encStorage,
  126. \OC\Encryption\Util $util,
  127. \OC\Encryption\File $file,
  128. $mode,
  129. $size,
  130. $unencryptedSize,
  131. $headerSize,
  132. $signed,
  133. $wrapper = 'OC\Files\Stream\Encryption') {
  134. $context = stream_context_create(array(
  135. 'ocencryption' => array(
  136. 'source' => $source,
  137. 'storage' => $storage,
  138. 'internalPath' => $internalPath,
  139. 'fullPath' => $fullPath,
  140. 'encryptionModule' => $encryptionModule,
  141. 'header' => $header,
  142. 'uid' => $uid,
  143. 'util' => $util,
  144. 'file' => $file,
  145. 'size' => $size,
  146. 'unencryptedSize' => $unencryptedSize,
  147. 'encryptionStorage' => $encStorage,
  148. 'headerSize' => $headerSize,
  149. 'signed' => $signed
  150. )
  151. ));
  152. return self::wrapSource($source, $context, 'ocencryption', $wrapper, $mode);
  153. }
  154. /**
  155. * add stream wrapper
  156. *
  157. * @param resource $source
  158. * @param string $mode
  159. * @param resource $context
  160. * @param string $protocol
  161. * @param string $class
  162. * @return resource
  163. * @throws \BadMethodCallException
  164. */
  165. protected static function wrapSource($source, $context, $protocol, $class, $mode = 'r+') {
  166. try {
  167. stream_wrapper_register($protocol, $class);
  168. if (@rewinddir($source) === false) {
  169. $wrapped = fopen($protocol . '://', $mode, false, $context);
  170. } else {
  171. $wrapped = opendir($protocol . '://', $context);
  172. }
  173. } catch (\BadMethodCallException $e) {
  174. stream_wrapper_unregister($protocol);
  175. throw $e;
  176. }
  177. stream_wrapper_unregister($protocol);
  178. return $wrapped;
  179. }
  180. /**
  181. * Load the source from the stream context and return the context options
  182. *
  183. * @param string $name
  184. * @return array
  185. * @throws \BadMethodCallException
  186. */
  187. protected function loadContext($name) {
  188. $context = parent::loadContext($name);
  189. foreach ($this->expectedContextProperties as $property) {
  190. if (array_key_exists($property, $context)) {
  191. $this->{$property} = $context[$property];
  192. } else {
  193. throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set');
  194. }
  195. }
  196. return $context;
  197. }
  198. public function stream_open($path, $mode, $options, &$opened_path) {
  199. $this->loadContext('ocencryption');
  200. $this->position = 0;
  201. $this->cache = '';
  202. $this->writeFlag = false;
  203. $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
  204. if (
  205. $mode === 'w'
  206. || $mode === 'w+'
  207. || $mode === 'wb'
  208. || $mode === 'wb+'
  209. || $mode === 'r+'
  210. || $mode === 'rb+'
  211. ) {
  212. $this->readOnly = false;
  213. } else {
  214. $this->readOnly = true;
  215. }
  216. $sharePath = $this->fullPath;
  217. if (!$this->storage->file_exists($this->internalPath)) {
  218. $sharePath = dirname($sharePath);
  219. }
  220. $accessList = $this->file->getAccessList($sharePath);
  221. $this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
  222. if (
  223. $mode === 'w'
  224. || $mode === 'w+'
  225. || $mode === 'wb'
  226. || $mode === 'wb+'
  227. ) {
  228. // We're writing a new file so start write counter with 0 bytes
  229. $this->unencryptedSize = 0;
  230. $this->writeHeader();
  231. $this->headerSize = $this->util->getHeaderSize();
  232. $this->size = $this->headerSize;
  233. } else {
  234. $this->skipHeader();
  235. }
  236. return true;
  237. }
  238. public function stream_eof() {
  239. return $this->position >= $this->unencryptedSize;
  240. }
  241. public function stream_read($count) {
  242. $result = '';
  243. $count = min($count, $this->unencryptedSize - $this->position);
  244. while ($count > 0) {
  245. $remainingLength = $count;
  246. // update the cache of the current block
  247. $this->readCache();
  248. // determine the relative position in the current block
  249. $blockPosition = ($this->position % $this->unencryptedBlockSize);
  250. // if entire read inside current block then only position needs to be updated
  251. if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
  252. $result .= substr($this->cache, $blockPosition, $remainingLength);
  253. $this->position += $remainingLength;
  254. $count = 0;
  255. // otherwise remainder of current block is fetched, the block is flushed and the position updated
  256. } else {
  257. $result .= substr($this->cache, $blockPosition);
  258. $this->flush();
  259. $this->position += ($this->unencryptedBlockSize - $blockPosition);
  260. $count -= ($this->unencryptedBlockSize - $blockPosition);
  261. }
  262. }
  263. return $result;
  264. }
  265. public function stream_write($data) {
  266. $length = 0;
  267. // loop over $data to fit it in 6126 sized unencrypted blocks
  268. while (isset($data[0])) {
  269. $remainingLength = strlen($data);
  270. // set the cache to the current 6126 block
  271. $this->readCache();
  272. // for seekable streams the pointer is moved back to the beginning of the encrypted block
  273. // flush will start writing there when the position moves to another block
  274. $positionInFile = (int)floor($this->position / $this->unencryptedBlockSize) *
  275. $this->util->getBlockSize() + $this->headerSize;
  276. $resultFseek = $this->parentStreamSeek($positionInFile);
  277. // only allow writes on seekable streams, or at the end of the encrypted stream
  278. if (!($this->readOnly) && ($resultFseek || $positionInFile === $this->size)) {
  279. // switch the writeFlag so flush() will write the block
  280. $this->writeFlag = true;
  281. // determine the relative position in the current block
  282. $blockPosition = ($this->position % $this->unencryptedBlockSize);
  283. // check if $data fits in current block
  284. // if so, overwrite existing data (if any)
  285. // update position and liberate $data
  286. if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) {
  287. $this->cache = substr($this->cache, 0, $blockPosition)
  288. . $data . substr($this->cache, $blockPosition + $remainingLength);
  289. $this->position += $remainingLength;
  290. $length += $remainingLength;
  291. $data = '';
  292. // if $data doesn't fit the current block, the fill the current block and reiterate
  293. // after the block is filled, it is flushed and $data is updatedxxx
  294. } else {
  295. $this->cache = substr($this->cache, 0, $blockPosition) .
  296. substr($data, 0, $this->unencryptedBlockSize - $blockPosition);
  297. $this->flush();
  298. $this->position += ($this->unencryptedBlockSize - $blockPosition);
  299. $length += ($this->unencryptedBlockSize - $blockPosition);
  300. $data = substr($data, $this->unencryptedBlockSize - $blockPosition);
  301. }
  302. } else {
  303. $data = '';
  304. }
  305. $this->unencryptedSize = max($this->unencryptedSize, $this->position);
  306. }
  307. return $length;
  308. }
  309. public function stream_tell() {
  310. return $this->position;
  311. }
  312. public function stream_seek($offset, $whence = SEEK_SET) {
  313. $return = false;
  314. switch ($whence) {
  315. case SEEK_SET:
  316. $newPosition = $offset;
  317. break;
  318. case SEEK_CUR:
  319. $newPosition = $this->position + $offset;
  320. break;
  321. case SEEK_END:
  322. $newPosition = $this->unencryptedSize + $offset;
  323. break;
  324. default:
  325. return $return;
  326. }
  327. if ($newPosition > $this->unencryptedSize || $newPosition < 0) {
  328. return $return;
  329. }
  330. $newFilePosition = floor($newPosition / $this->unencryptedBlockSize)
  331. * $this->util->getBlockSize() + $this->headerSize;
  332. $oldFilePosition = parent::stream_tell();
  333. if ($this->parentStreamSeek($newFilePosition)) {
  334. $this->parentStreamSeek($oldFilePosition);
  335. $this->flush();
  336. $this->parentStreamSeek($newFilePosition);
  337. $this->position = $newPosition;
  338. $return = true;
  339. }
  340. return $return;
  341. }
  342. public function stream_close() {
  343. $this->flush('end');
  344. $position = (int)floor($this->position/$this->unencryptedBlockSize);
  345. $remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
  346. if ($this->readOnly === false) {
  347. if(!empty($remainingData)) {
  348. parent::stream_write($remainingData);
  349. }
  350. $this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize);
  351. }
  352. return parent::stream_close();
  353. }
  354. /**
  355. * write block to file
  356. * @param string $positionPrefix
  357. */
  358. protected function flush($positionPrefix = '') {
  359. // write to disk only when writeFlag was set to 1
  360. if ($this->writeFlag) {
  361. // Disable the file proxies so that encryption is not
  362. // automatically attempted when the file is written to disk -
  363. // we are handling that separately here and we don't want to
  364. // get into an infinite loop
  365. $position = (int)floor($this->position/$this->unencryptedBlockSize);
  366. $encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
  367. $bytesWritten = parent::stream_write($encrypted);
  368. $this->writeFlag = false;
  369. // Check whether the write concerns the last block
  370. // If so then update the encrypted filesize
  371. // Note that the unencrypted pointer and filesize are NOT yet updated when flush() is called
  372. // We recalculate the encrypted filesize as we do not know the context of calling flush()
  373. $completeBlocksInFile=(int)floor($this->unencryptedSize/$this->unencryptedBlockSize);
  374. if ($completeBlocksInFile === (int)floor($this->position/$this->unencryptedBlockSize)) {
  375. $this->size = $this->util->getBlockSize() * $completeBlocksInFile;
  376. $this->size += $bytesWritten;
  377. $this->size += $this->headerSize;
  378. }
  379. }
  380. // always empty the cache (otherwise readCache() will not fill it with the new block)
  381. $this->cache = '';
  382. }
  383. /**
  384. * read block to file
  385. */
  386. protected function readCache() {
  387. // cache should always be empty string when this function is called
  388. // don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block
  389. if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
  390. // Get the data from the file handle
  391. $data = parent::stream_read($this->util->getBlockSize());
  392. $position = (int)floor($this->position/$this->unencryptedBlockSize);
  393. $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
  394. if($numberOfChunks === $position) {
  395. $position .= 'end';
  396. }
  397. $this->cache = $this->encryptionModule->decrypt($data, $position);
  398. }
  399. }
  400. /**
  401. * write header at beginning of encrypted file
  402. *
  403. * @return integer
  404. * @throws EncryptionHeaderKeyExistsException if header key is already in use
  405. */
  406. protected function writeHeader() {
  407. $header = $this->util->createHeader($this->newHeader, $this->encryptionModule);
  408. return parent::stream_write($header);
  409. }
  410. /**
  411. * read first block to skip the header
  412. */
  413. protected function skipHeader() {
  414. parent::stream_read($this->headerSize);
  415. }
  416. /**
  417. * call stream_seek() from parent class
  418. *
  419. * @param integer $position
  420. * @return bool
  421. */
  422. protected function parentStreamSeek($position) {
  423. return parent::stream_seek($position);
  424. }
  425. /**
  426. * @param string $path
  427. * @param array $options
  428. * @return bool
  429. */
  430. public function dir_opendir($path, $options) {
  431. return false;
  432. }
  433. }