From f2d7fad0fb6263a0b93ed91f4f82b9e8a1acd26e Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Wed, 17 Dec 2014 11:00:16 +0100 Subject: [PATCH 1/6] put Favorites second in list, after 'All files' --- apps/files/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/index.php b/apps/files/index.php index 02076226c1..64b49c3bf1 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -93,7 +93,7 @@ function sortNavigationItems($item1, $item2) { 'id' => 'favorites', 'appname' => 'files', 'script' => 'simplelist.php', - 'order' => 50, + 'order' => 5, 'name' => $l->t('Favorites') ) ); From 433d1de923392c005193e99ad1d8efdf2513544f Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Wed, 17 Dec 2014 11:04:18 +0100 Subject: [PATCH 2/6] fix empty state for Favorites --- apps/files/css/files.css | 4 ++++ apps/files/templates/simplelist.php | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 3829759a14..cf166003fb 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -556,6 +556,10 @@ table tr.summary td { #scanning-message{ top:40%; left:40%; position:absolute; display:none; } +#emptycontent .icon-starred { + height: 50px; +} + table.dragshadow { width:auto; } diff --git a/apps/files/templates/simplelist.php b/apps/files/templates/simplelist.php index c00febce65..15949bf704 100644 --- a/apps/files/templates/simplelist.php +++ b/apps/files/templates/simplelist.php @@ -3,7 +3,10 @@
- + From c7cf0d0386036aba138a9279e09ea09a1fc206a1 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Wed, 17 Dec 2014 11:08:39 +0100 Subject: [PATCH 3/6] fix file list summary left alignment --- apps/files/css/mobile.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/css/mobile.css b/apps/files/css/mobile.css index 780b7ac844..4881f7c70e 100644 --- a/apps/files/css/mobile.css +++ b/apps/files/css/mobile.css @@ -24,7 +24,7 @@ table td { } /* and accordingly fix left margin of file list summary on mobile */ .summary .info { - margin-left: 55px; + margin-left: 105px; } /* remove shift for multiselect bar to account for missing navigation */ From e2977ff4c1e7b5b1c678a91550bc3e42ffd26d64 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Wed, 17 Dec 2014 13:40:57 +0100 Subject: [PATCH 4/6] fix favorite star flickering on empty state page --- apps/files/css/files.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index cf166003fb..eb6fec4a97 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -557,7 +557,9 @@ table tr.summary td { #scanning-message{ top:40%; left:40%; position:absolute; display:none; } #emptycontent .icon-starred { - height: 50px; + height: 30px; + width: 30px; + margin: 0 auto 10px; } table.dragshadow { From e426375114eef2ad151dcd58d62b928c5228ec51 Mon Sep 17 00:00:00 2001 From: jknockaert Date: Wed, 17 Dec 2014 15:48:05 +0100 Subject: [PATCH 5/6] update enc stream --- apps/files_encryption/lib/stream.php | 293 +++++++++++++++------------ 1 file changed, 159 insertions(+), 134 deletions(-) diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php index 17da4eb1cd..f64534d205 100644 --- a/apps/files_encryption/lib/stream.php +++ b/apps/files_encryption/lib/stream.php @@ -64,15 +64,17 @@ class Stream { private $keyId; private $handle; // Resource returned by fopen private $meta = array(); // Header / meta for source stream - private $writeCache; + private $cache; // Current block unencrypted + private $position; // Current pointer position in the unencrypted stream + private $writeFlag; // Flag to write current block when leaving it private $size; + private $headerSize = 0; // Size of header private $unencryptedSize; private $publicKey; private $encKeyfile; private $newFile; // helper var, we only need to write the keyfile for new files private $isLocalTmpFile = false; // do we operate on a local tmp file private $localTmpFile; // path of local tmp file - private $headerWritten = false; private $containHeader = false; // the file contain a header private $cipher; // cipher used for encryption/decryption @@ -148,6 +150,17 @@ class Stream { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; + $this->position = 0; + $this->cache = ''; + $this->writeFlag = 0; + + // Setting handle so it can be used for reading the header + if ($this->isLocalTmpFile) { + $this->handle = fopen($this->localTmpFile, $mode); + } else { + $this->handle = $this->rootView->fopen($this->rawPath, $mode); + } + if ( $mode === 'w' or $mode === 'w+' @@ -168,16 +181,14 @@ class Stream { $this->size = $this->rootView->filesize($this->rawPath); + \OC_FileProxy::$enabled = true; + $this->unencryptedSize = $this->rootView->filesize($this->rawPath); + \OC_FileProxy::$enabled = false; + $this->readHeader(); } - if ($this->isLocalTmpFile) { - $this->handle = fopen($this->localTmpFile, $mode); - } else { - $this->handle = $this->rootView->fopen($this->rawPath, $mode); - } - \OC_FileProxy::$enabled = $proxyStatus; if (!is_resource($this->handle)) { @@ -200,14 +211,8 @@ class Stream { private function readHeader() { - if ($this->isLocalTmpFile) { - $handle = fopen($this->localTmpFile, 'r'); - } else { - $handle = $this->rootView->fopen($this->rawPath, 'r'); - } - - if (is_resource($handle)) { - $data = fread($handle, Crypt::BLOCKSIZE); + if (is_resource($this->handle)) { + $data = fread($this->handle, Crypt::BLOCKSIZE); $header = Crypt::parseHeader($data); $this->cipher = Crypt::getCipher($header); @@ -215,9 +220,16 @@ class Stream { // remeber that we found a header if (!empty($header)) { $this->containHeader = true; + $this->headerSize = Crypt::BLOCKSIZE; + // if there's no header then decrypt the block and store it in the cache + } else { + if (!$this->getKey()) { + throw new \Exception('Encryption key not found for "' . $this->rawPath . '" during attempted read via stream'); + } else { + $this->cache = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); + } } - fclose($handle); } } @@ -226,7 +238,7 @@ class Stream { * @return int position of the file pointer */ public function stream_tell() { - return ftell($this->handle); + return $this->position; } /** @@ -234,18 +246,40 @@ class Stream { * @param int $whence * @return bool true if fseek was successful, otherwise false */ + + // seeking the stream tries to move the pointer on the encrypted stream to the beginning of the target block + // if that works, it flushes the current block and changes the position in the unencrypted stream public function stream_seek($offset, $whence = SEEK_SET) { - - $this->flush(); - - // ignore the header and just overstep it - if ($this->containHeader) { - $offset += Crypt::BLOCKSIZE; - } - // this wrapper needs to return "true" for success. // the fseek call itself returns 0 on succeess - return !fseek($this->handle, $offset, $whence); + $return=false; + + switch($whence) { + case SEEK_SET: + if($offset < $this->unencryptedSize && $offset >= 0) { + $newPosition=$offset; + } + break; + case SEEK_CUR: + if($offset>=0) { + $newPosition=$offset+$this->position; + } + break; + case SEEK_END: + if($this->unencryptedSize + $offset >= 0) { + $newPosition=$this->unencryptedSize+$offset; + } + break; + default: + return $return; + } + $newFilePosition=floor($newPosition/6126)*Crypt::BLOCKSIZE+$this->headerSize; + if (fseek($this->handle, $newFilePosition)===0) { + $this->flush(); + $this->position=$newPosition; + $return=true; + } + return $return; } @@ -256,35 +290,32 @@ class Stream { */ public function stream_read($count) { - $this->writeCache = ''; + $result = ''; + // limit to the end of the unencrypted file; otherwise getFileSize will fail and it is good practise anyway + $count=min($count,$this->unencryptedSize - $this->position); - if ($count !== Crypt::BLOCKSIZE) { - \OCP\Util::writeLog('Encryption library', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL); - throw new EncryptionException('expected a blog size of 8192 byte', EncryptionException::UNEXPECTED_BLOG_SIZE); - } + // loop over the 6126 sized unencrypted blocks + while ($count > 0) { - // Get the data from the file handle - $data = fread($this->handle, $count); + $remainingLength = $count; - // if this block contained the header we move on to the next block - if (Crypt::isHeader($data)) { - $data = fread($this->handle, $count); - } - - $result = null; - - if (strlen($data)) { - - if (!$this->getKey()) { - - // Error! We don't have a key to decrypt the file with - throw new \Exception( - 'Encryption key not found for "' . $this->rawPath . '" during attempted read via stream'); + // update the cache of the current block + $this->readCache(); + + // determine the relative position in the current block + $blockPosition=($this->position % 6126); + // if entire read inside current block then only position needs to be updated + if ($remainingLength<(6126 - $blockPosition)) { + $result .= substr($this->cache,$blockPosition,$remainingLength); + $this->position += $remainingLength; + $count=0; + // otherwise remainder of current block is fetched, the block is flushed and the position updated } else { - - // Decrypt data - $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); + $result .= substr($this->cache,$blockPosition); + $this->flush(); + $this->position += (6126 - $blockPosition); + $count -= (6126 - $blockPosition); } } @@ -381,7 +412,9 @@ class Stream { $paddedHeader = str_pad($header, Crypt::BLOCKSIZE, self::PADDING_CHAR, STR_PAD_RIGHT); fwrite($this->handle, $paddedHeader); - $this->headerWritten = true; + $this->containHeader = true; + $this->headerSize = Crypt::BLOCKSIZE; + $this->size += $this->headerSize; } /** @@ -389,7 +422,7 @@ class Stream { * @param string $data data to be written to disk * @note the data will be written to the path stored in the stream handle, set in stream_open() * @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 - * @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 + * @note Because the encryption process used increases the length of $data, a cache is used to carry over data which would not fit in the required block size * @note Padding is added to each encrypted block to ensure that the resulting block is exactly 8192 bytes. This is removed during stream_read * @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 */ @@ -400,102 +433,68 @@ class Stream { $this->size = 0; return strlen($data); } - - if ($this->headerWritten === false) { + + if ($this->size === 0) { $this->writeHeader(); } - // Disable the file proxies so that encryption is not - // automatically attempted when the file is written to disk - - // we are handling that separately here and we don't want to - // get into an infinite loop - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; - - // Get the length of the unencrypted data that we are handling - $length = strlen($data); - - // Find out where we are up to in the writing of data to the - // file - $pointer = ftell($this->handle); - // Get / generate the keyfile for the file we're handling // If we're writing a new file (not overwriting an existing // one), save the newly generated keyfile + if (!$this->getKey()) { - $this->plainKey = Crypt::generateKey(); - } - // If extra data is left over from the last round, make sure it - // is integrated into the next 6126 / 8192 block - if ($this->writeCache) { + $length=0; - // Concat writeCache to start of $data - $data = $this->writeCache . $data; - - // Clear the write cache, ready for reuse - it has been - // flushed and its old contents processed - $this->writeCache = ''; - - } - - // While there still remains some data to be processed & written + // loop over $data to fit it in 6126 sized unencrypted blocks while (strlen($data) > 0) { - // Remaining length for this iteration, not of the - // entire file (may be greater than 8192 bytes) $remainingLength = strlen($data); - // If data remaining to be written is less than the - // size of 1 6126 byte block - if ($remainingLength < 6126) { + // set the cache to the current 6126 block + $this->readCache(); - // Set writeCache to contents of $data - // The writeCache will be carried over to the - // next write round, and added to the start of - // $data to ensure that written blocks are - // always the correct length. If there is still - // data in writeCache after the writing round - // has finished, then the data will be written - // to disk by $this->flush(). - $this->writeCache = $data; + // only allow writes on seekable streams, or at the end of the encrypted stream + // for seekable streams the pointer is moved back to the beginning of the encrypted block + // flush will start writing there when the position moves to another block + if((fseek($this->handle, floor($this->position/6126)*Crypt::BLOCKSIZE + $this->headerSize) === 0) || (floor($this->position/6126)*Crypt::BLOCKSIZE + $this->headerSize === $this->size)) { - // Clear $data ready for next round - $data = ''; + // switch the writeFlag so flush() will write the block + $this->writeFlag=1; + + // determine the relative position in the current block + $blockPosition=($this->position % 6126); + + // check if $data fits in current block + // if so, overwrite existing data (if any) + // update position and liberate $data + if ($remainingLength<(6126 - $blockPosition)) { + $this->cache=substr($this->cache,0,$blockPosition).$data.substr($this->cache,$blockPosition+$remainingLength); + $this->position += $remainingLength; + $length += $remainingLength; + $data = ''; + // if $data doens't fit the current block, the fill the current block and reiterate + // after the block is filled, it is flushed and $data is updated + } else { + $this->cache=substr($this->cache,0,$blockPosition).substr($data,0,6126-$blockPosition); + $this->flush(); + $this->position += (6126 - $blockPosition); + $length += (6126 - $blockPosition); + $data = substr($data, 6126 - $blockPosition); + } } else { - - // Read the chunk from the start of $data - $chunk = substr($data, 0, 6126); - - $encrypted = $this->preWriteEncrypt($chunk, $this->plainKey); - - // Write the data chunk to disk. This will be - // attended to the last data chunk if the file - // being handled totals more than 6126 bytes - fwrite($this->handle, $encrypted); - - // Remove the chunk we just processed from - // $data, leaving only unprocessed data in $data - // var, for handling on the next round - $data = substr($data, 6126); - + $data=''; } - } - $this->size = max($this->size, $pointer + $length); - $this->unencryptedSize += $length; - - \OC_FileProxy::$enabled = $proxyStatus; - + $this->unencryptedSize = max($this->unencryptedSize,$this->position); return $length; } - /** * @param int $option * @param int $arg1 @@ -535,7 +534,7 @@ class Stream { * @return bool */ public function stream_flush() { - + $this->flush(); return fflush($this->handle); // Not a typo: http://php.net/manual/en/function.fflush.php @@ -545,21 +544,47 @@ class Stream { * @return bool */ public function stream_eof() { - return feof($this->handle); + return ($this->position>=$this->unencryptedSize); } private function flush() { - - if ($this->writeCache) { - + // write to disk only when writeFlag was set to 1 + if ($this->writeFlag === 1) { + // Disable the file proxies so that encryption is not + // automatically attempted when the file is written to disk - + // we are handling that separately here and we don't want to + // get into an infinite loop + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; // Set keyfile property for file in question $this->getKey(); - - $encrypted = $this->preWriteEncrypt($this->writeCache, $this->plainKey); - + $encrypted = $this->preWriteEncrypt($this->cache, $this->plainKey); fwrite($this->handle, $encrypted); + $this->writeFlag = 0; + $this->size = max($this->size,ftell($this->handle)); + \OC_FileProxy::$enabled = $proxyStatus; + } + // always empty the cache (otherwise readCache() will not fill it with the new block) + $this->cache = ''; + } - $this->writeCache = ''; + private function readCache() { + // cache should always be empty string when this function is called + // don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block + if ($this->cache === '' && !($this->position===$this->unencryptedSize && ($this->position % 6126)===0)) { + // Get the data from the file handle + $data = fread($this->handle, Crypt::BLOCKSIZE); + $result = ''; + if (strlen($data)) { + if (!$this->getKey()) { + // Error! We don't have a key to decrypt the file with + throw new \Exception('Encryption key not found for "'. $this->rawPath . '" during attempted read via stream'); + } else { + // Decrypt data + $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); + } + } + $this->cache = $result; } } @@ -580,7 +605,7 @@ class Stream { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - if ($this->rootView->file_exists($this->rawPath) && $this->size === 0) { + if ($this->rootView->file_exists($this->rawPath) && $this->size === $this->headerSize) { fclose($this->handle); $this->rootView->unlink($this->rawPath); } @@ -597,7 +622,7 @@ class Stream { $this->meta['mode'] !== 'r' && $this->meta['mode'] !== 'rb' && $this->isLocalTmpFile === false && - $this->size > 0 && + $this->size > $this->headerSize && $this->unencryptedSize > 0 ) { From 470994ba1eb89edf461f93726d51047fcf9665b6 Mon Sep 17 00:00:00 2001 From: jknockaert Date: Thu, 19 Feb 2015 15:43:22 +0100 Subject: [PATCH 6/6] OC8 version --- apps/files_encryption/lib/stream.php | 364 +++++++++++++-------------- 1 file changed, 169 insertions(+), 195 deletions(-) diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php index f64534d205..5a0fb155c3 100644 --- a/apps/files_encryption/lib/stream.php +++ b/apps/files_encryption/lib/stream.php @@ -64,19 +64,19 @@ class Stream { private $keyId; private $handle; // Resource returned by fopen private $meta = array(); // Header / meta for source stream - private $cache; // Current block unencrypted - private $position; // Current pointer position in the unencrypted stream - private $writeFlag; // Flag to write current block when leaving it + private $writeCache; private $size; - private $headerSize = 0; // Size of header private $unencryptedSize; private $publicKey; private $encKeyfile; private $newFile; // helper var, we only need to write the keyfile for new files private $isLocalTmpFile = false; // do we operate on a local tmp file private $localTmpFile; // path of local tmp file + private $headerWritten = false; private $containHeader = false; // the file contain a header private $cipher; // cipher used for encryption/decryption + /** @var \OCA\Files_Encryption\Util */ + private $util; /** * @var \OC\Files\View @@ -105,9 +105,7 @@ class Stream { // assume that the file already exist before we decide it finally in getKey() $this->newFile = false; - if (!isset($this->rootView)) { - $this->rootView = new \OC\Files\View('/'); - } + $this->rootView = new \OC\Files\View('/'); $this->session = new Session($this->rootView); @@ -118,7 +116,8 @@ class Stream { } $normalizedPath = \OC\Files\Filesystem::normalizePath(str_replace('crypt://', '', $path)); - if ($originalFile = Helper::getPathFromTmpFile($normalizedPath)) { + $originalFile = Helper::getPathFromTmpFile($normalizedPath); + if ($originalFile) { $this->rawPath = $originalFile; $this->isLocalTmpFile = true; $this->localTmpFile = $normalizedPath; @@ -126,67 +125,57 @@ class Stream { $this->rawPath = $normalizedPath; } - $this->userId = Helper::getUser($this->rawPath); - - $util = new Util($this->rootView, $this->userId); + $this->util = new Util($this->rootView, Helper::getUser($this->rawPath)); // get the key ID which we want to use, can be the users key or the // public share key - $this->keyId = $util->getKeyId(); + $this->keyId = $this->util->getKeyId(); - // Strip identifier text from path, this gives us the path relative to data//files - $this->relPath = Helper::stripUserFilesPath($this->rawPath); - // if raw path doesn't point to a real file, check if it is a version or a file in the trash bin - if ($this->relPath === false) { - $this->relPath = Helper::getPathToRealFile($this->rawPath); - } + $fileType = Helper::detectFileType($this->rawPath); - if($this->relPath === false) { - \OCP\Util::writeLog('Encryption library', 'failed to open file "' . $this->rawPath . '" expecting a path to "files", "files_versions" or "cache"', \OCP\Util::ERROR); - return false; + switch ($fileType) { + case Util::FILE_TYPE_FILE: + $this->relPath = Helper::stripUserFilesPath($this->rawPath); + $user = \OC::$server->getUserSession()->getUser(); + $this->userId = $user ? $user->getUID() : Helper::getUserFromPath($this->rawPath); + break; + case Util::FILE_TYPE_VERSION: + $this->relPath = Helper::getPathFromVersion($this->rawPath); + $this->userId = Helper::getUserFromPath($this->rawPath); + break; + case Util::FILE_TYPE_CACHE: + $this->relPath = Helper::getPathFromCachedFile($this->rawPath); + Helper::mkdirr($this->rawPath, new \OC\Files\View('/')); + $user = \OC::$server->getUserSession()->getUser(); + $this->userId = $user ? $user->getUID() : Helper::getUserFromPath($this->rawPath); + break; + default: + \OCP\Util::writeLog('Encryption library', 'failed to open file "' . $this->rawPath . '" expecting a path to "files", "files_versions" or "cache"', \OCP\Util::ERROR); + return false; } // Disable fileproxies so we can get the file size and open the source file without recursive encryption $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - $this->position = 0; - $this->cache = ''; - $this->writeFlag = 0; - - // Setting handle so it can be used for reading the header - if ($this->isLocalTmpFile) { - $this->handle = fopen($this->localTmpFile, $mode); - } else { - $this->handle = $this->rootView->fopen($this->rawPath, $mode); - } - if ( $mode === 'w' or $mode === 'w+' or $mode === 'wb' or $mode === 'wb+' ) { - // We're writing a new file so start write counter with 0 bytes $this->size = 0; $this->unencryptedSize = 0; - } else { - - if($this->privateKey === false) { - // if private key is not valid redirect user to a error page - Helper::redirectToErrorPage($this->session); - } - $this->size = $this->rootView->filesize($this->rawPath); - - \OC_FileProxy::$enabled = true; - $this->unencryptedSize = $this->rootView->filesize($this->rawPath); - \OC_FileProxy::$enabled = false; - $this->readHeader(); + } + if ($this->isLocalTmpFile) { + $this->handle = fopen($this->localTmpFile, $mode); + } else { + $this->handle = $this->rootView->fopen($this->rawPath, $mode); } \OC_FileProxy::$enabled = $proxyStatus; @@ -211,8 +200,14 @@ class Stream { private function readHeader() { - if (is_resource($this->handle)) { - $data = fread($this->handle, Crypt::BLOCKSIZE); + if ($this->isLocalTmpFile) { + $handle = fopen($this->localTmpFile, 'r'); + } else { + $handle = $this->rootView->fopen($this->rawPath, 'r'); + } + + if (is_resource($handle)) { + $data = fread($handle, Crypt::BLOCKSIZE); $header = Crypt::parseHeader($data); $this->cipher = Crypt::getCipher($header); @@ -220,16 +215,9 @@ class Stream { // remeber that we found a header if (!empty($header)) { $this->containHeader = true; - $this->headerSize = Crypt::BLOCKSIZE; - // if there's no header then decrypt the block and store it in the cache - } else { - if (!$this->getKey()) { - throw new \Exception('Encryption key not found for "' . $this->rawPath . '" during attempted read via stream'); - } else { - $this->cache = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); - } } + fclose($handle); } } @@ -238,7 +226,7 @@ class Stream { * @return int position of the file pointer */ public function stream_tell() { - return $this->position; + return ftell($this->handle); } /** @@ -246,40 +234,18 @@ class Stream { * @param int $whence * @return bool true if fseek was successful, otherwise false */ - - // seeking the stream tries to move the pointer on the encrypted stream to the beginning of the target block - // if that works, it flushes the current block and changes the position in the unencrypted stream public function stream_seek($offset, $whence = SEEK_SET) { + + $this->flush(); + + // ignore the header and just overstep it + if ($this->containHeader) { + $offset += Crypt::BLOCKSIZE; + } + // this wrapper needs to return "true" for success. // the fseek call itself returns 0 on succeess - $return=false; - - switch($whence) { - case SEEK_SET: - if($offset < $this->unencryptedSize && $offset >= 0) { - $newPosition=$offset; - } - break; - case SEEK_CUR: - if($offset>=0) { - $newPosition=$offset+$this->position; - } - break; - case SEEK_END: - if($this->unencryptedSize + $offset >= 0) { - $newPosition=$this->unencryptedSize+$offset; - } - break; - default: - return $return; - } - $newFilePosition=floor($newPosition/6126)*Crypt::BLOCKSIZE+$this->headerSize; - if (fseek($this->handle, $newFilePosition)===0) { - $this->flush(); - $this->position=$newPosition; - $return=true; - } - return $return; + return !fseek($this->handle, $offset, $whence); } @@ -290,32 +256,35 @@ class Stream { */ public function stream_read($count) { - $result = ''; - // limit to the end of the unencrypted file; otherwise getFileSize will fail and it is good practise anyway - $count=min($count,$this->unencryptedSize - $this->position); + $this->writeCache = ''; - // loop over the 6126 sized unencrypted blocks - while ($count > 0) { + if ($count !== Crypt::BLOCKSIZE) { + \OCP\Util::writeLog('Encryption library', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL); + throw new EncryptionException('expected a block size of 8192 byte', EncryptionException::UNEXPECTED_BLOCK_SIZE); + } - $remainingLength = $count; + // Get the data from the file handle + $data = fread($this->handle, $count); - // update the cache of the current block - $this->readCache(); - - // determine the relative position in the current block - $blockPosition=($this->position % 6126); + // if this block contained the header we move on to the next block + if (Crypt::isHeader($data)) { + $data = fread($this->handle, $count); + } + + $result = null; + + if (strlen($data)) { + + if (!$this->getKey()) { + + // Error! We don't have a key to decrypt the file with + throw new \Exception( + 'Encryption key not found for "' . $this->rawPath . '" during attempted read via stream'); - // if entire read inside current block then only position needs to be updated - if ($remainingLength<(6126 - $blockPosition)) { - $result .= substr($this->cache,$blockPosition,$remainingLength); - $this->position += $remainingLength; - $count=0; - // otherwise remainder of current block is fetched, the block is flushed and the position updated } else { - $result .= substr($this->cache,$blockPosition); - $this->flush(); - $this->position += (6126 - $blockPosition); - $count -= (6126 - $blockPosition); + + // Decrypt data + $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); } } @@ -359,15 +328,16 @@ class Stream { } + // $util = new Util($this->rootView, $this->userId); + // Fetch and decrypt keyfile // Fetch existing keyfile - $util = new Util($this->rootView, $this->userId); - $this->encKeyfile = Keymanager::getFileKey($this->rootView, $util, $this->relPath); + $this->encKeyfile = Keymanager::getFileKey($this->rootView, $this->util, $this->relPath); // If a keyfile already exists if ($this->encKeyfile) { - $shareKey = Keymanager::getShareKey($this->rootView, $this->keyId, $util, $this->relPath); + $shareKey = Keymanager::getShareKey($this->rootView, $this->keyId, $this->util, $this->relPath); // if there is no valid private key return false if ($this->privateKey === false) { @@ -412,9 +382,7 @@ class Stream { $paddedHeader = str_pad($header, Crypt::BLOCKSIZE, self::PADDING_CHAR, STR_PAD_RIGHT); fwrite($this->handle, $paddedHeader); - $this->containHeader = true; - $this->headerSize = Crypt::BLOCKSIZE; - $this->size += $this->headerSize; + $this->headerWritten = true; } /** @@ -422,7 +390,7 @@ class Stream { * @param string $data data to be written to disk * @note the data will be written to the path stored in the stream handle, set in stream_open() * @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 - * @note Because the encryption process used increases the length of $data, a cache is used to carry over data which would not fit in the required block size + * @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 * @note Padding is added to each encrypted block to ensure that the resulting block is exactly 8192 bytes. This is removed during stream_read * @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 */ @@ -433,68 +401,102 @@ class Stream { $this->size = 0; return strlen($data); } - - if ($this->size === 0) { + + if ($this->headerWritten === false) { $this->writeHeader(); } + // Disable the file proxies so that encryption is not + // automatically attempted when the file is written to disk - + // we are handling that separately here and we don't want to + // get into an infinite loop + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Get the length of the unencrypted data that we are handling + $length = strlen($data); + + // Find out where we are up to in the writing of data to the + // file + $pointer = ftell($this->handle); + // Get / generate the keyfile for the file we're handling // If we're writing a new file (not overwriting an existing // one), save the newly generated keyfile - if (!$this->getKey()) { + $this->plainKey = Crypt::generateKey(); + } - $length=0; + // If extra data is left over from the last round, make sure it + // is integrated into the next 6126 / 8192 block + if ($this->writeCache) { - // loop over $data to fit it in 6126 sized unencrypted blocks + // Concat writeCache to start of $data + $data = $this->writeCache . $data; + + // Clear the write cache, ready for reuse - it has been + // flushed and its old contents processed + $this->writeCache = ''; + + } + + // While there still remains some data to be processed & written while (strlen($data) > 0) { + // Remaining length for this iteration, not of the + // entire file (may be greater than 8192 bytes) $remainingLength = strlen($data); - // set the cache to the current 6126 block - $this->readCache(); + // If data remaining to be written is less than the + // size of 1 6126 byte block + if ($remainingLength < 6126) { - // only allow writes on seekable streams, or at the end of the encrypted stream - // for seekable streams the pointer is moved back to the beginning of the encrypted block - // flush will start writing there when the position moves to another block - if((fseek($this->handle, floor($this->position/6126)*Crypt::BLOCKSIZE + $this->headerSize) === 0) || (floor($this->position/6126)*Crypt::BLOCKSIZE + $this->headerSize === $this->size)) { + // Set writeCache to contents of $data + // The writeCache will be carried over to the + // next write round, and added to the start of + // $data to ensure that written blocks are + // always the correct length. If there is still + // data in writeCache after the writing round + // has finished, then the data will be written + // to disk by $this->flush(). + $this->writeCache = $data; - // switch the writeFlag so flush() will write the block - $this->writeFlag=1; - - // determine the relative position in the current block - $blockPosition=($this->position % 6126); - - // check if $data fits in current block - // if so, overwrite existing data (if any) - // update position and liberate $data - if ($remainingLength<(6126 - $blockPosition)) { - $this->cache=substr($this->cache,0,$blockPosition).$data.substr($this->cache,$blockPosition+$remainingLength); - $this->position += $remainingLength; - $length += $remainingLength; - $data = ''; - // if $data doens't fit the current block, the fill the current block and reiterate - // after the block is filled, it is flushed and $data is updated - } else { - $this->cache=substr($this->cache,0,$blockPosition).substr($data,0,6126-$blockPosition); - $this->flush(); - $this->position += (6126 - $blockPosition); - $length += (6126 - $blockPosition); - $data = substr($data, 6126 - $blockPosition); - } + // Clear $data ready for next round + $data = ''; } else { - $data=''; + + // Read the chunk from the start of $data + $chunk = substr($data, 0, 6126); + + $encrypted = $this->preWriteEncrypt($chunk, $this->plainKey); + + // Write the data chunk to disk. This will be + // attended to the last data chunk if the file + // being handled totals more than 6126 bytes + fwrite($this->handle, $encrypted); + + // Remove the chunk we just processed from + // $data, leaving only unprocessed data in $data + // var, for handling on the next round + $data = substr($data, 6126); + } + } - $this->unencryptedSize = max($this->unencryptedSize,$this->position); + $this->size = max($this->size, $pointer + $length); + $this->unencryptedSize += $length; + + \OC_FileProxy::$enabled = $proxyStatus; + return $length; } + /** * @param int $option * @param int $arg1 @@ -534,7 +536,7 @@ class Stream { * @return bool */ public function stream_flush() { - $this->flush(); + return fflush($this->handle); // Not a typo: http://php.net/manual/en/function.fflush.php @@ -544,47 +546,21 @@ class Stream { * @return bool */ public function stream_eof() { - return ($this->position>=$this->unencryptedSize); + return feof($this->handle); } private function flush() { - // write to disk only when writeFlag was set to 1 - if ($this->writeFlag === 1) { - // Disable the file proxies so that encryption is not - // automatically attempted when the file is written to disk - - // we are handling that separately here and we don't want to - // get into an infinite loop - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; + + if ($this->writeCache) { + // Set keyfile property for file in question $this->getKey(); - $encrypted = $this->preWriteEncrypt($this->cache, $this->plainKey); - fwrite($this->handle, $encrypted); - $this->writeFlag = 0; - $this->size = max($this->size,ftell($this->handle)); - \OC_FileProxy::$enabled = $proxyStatus; - } - // always empty the cache (otherwise readCache() will not fill it with the new block) - $this->cache = ''; - } - private function readCache() { - // cache should always be empty string when this function is called - // don't try to fill the cache when trying to write at the end of the unencrypted file when it coincides with new block - if ($this->cache === '' && !($this->position===$this->unencryptedSize && ($this->position % 6126)===0)) { - // Get the data from the file handle - $data = fread($this->handle, Crypt::BLOCKSIZE); - $result = ''; - if (strlen($data)) { - if (!$this->getKey()) { - // Error! We don't have a key to decrypt the file with - throw new \Exception('Encryption key not found for "'. $this->rawPath . '" during attempted read via stream'); - } else { - // Decrypt data - $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); - } - } - $this->cache = $result; + $encrypted = $this->preWriteEncrypt($this->writeCache, $this->plainKey); + + fwrite($this->handle, $encrypted); + + $this->writeCache = ''; } } @@ -605,7 +581,7 @@ class Stream { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - if ($this->rootView->file_exists($this->rawPath) && $this->size === $this->headerSize) { + if ($this->rootView->file_exists($this->rawPath) && $this->size === 0) { fclose($this->handle); $this->rootView->unlink($this->rawPath); } @@ -622,7 +598,7 @@ class Stream { $this->meta['mode'] !== 'r' && $this->meta['mode'] !== 'rb' && $this->isLocalTmpFile === false && - $this->size > $this->headerSize && + $this->size > 0 && $this->unencryptedSize > 0 ) { @@ -639,11 +615,9 @@ class Stream { // Check if OC sharing api is enabled $sharingEnabled = \OCP\Share::isEnabled(); - $util = new Util($this->rootView, $this->userId); - // Get all users sharing the file includes current user - $uniqueUserIds = $util->getSharingUsersArray($sharingEnabled, $this->relPath); - $checkedUserIds = $util->filterShareReadyUsers($uniqueUserIds); + $uniqueUserIds = $this->util->getSharingUsersArray($sharingEnabled, $this->relPath); + $checkedUserIds = $this->util->filterShareReadyUsers($uniqueUserIds); // Fetch public keys for all sharing users $publicKeys = Keymanager::getPublicKeys($this->rootView, $checkedUserIds['ready']); @@ -652,10 +626,10 @@ class Stream { $this->encKeyfiles = Crypt::multiKeyEncrypt($this->plainKey, $publicKeys); // Save the new encrypted file key - Keymanager::setFileKey($this->rootView, $util, $this->relPath, $this->encKeyfiles['data']); + Keymanager::setFileKey($this->rootView, $this->util, $this->relPath, $this->encKeyfiles['data']); // Save the sharekeys - Keymanager::setShareKeys($this->rootView, $util, $this->relPath, $this->encKeyfiles['keys']); + Keymanager::setShareKeys($this->rootView, $this->util, $this->relPath, $this->encKeyfiles['keys']); // Re-enable proxy - our work is done \OC_FileProxy::$enabled = $proxyStatus;