diff options
Diffstat (limited to 'apps/files_encryption/lib/stream.php')
-rw-r--r-- | apps/files_encryption/lib/stream.php | 700 |
1 files changed, 0 insertions, 700 deletions
diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php deleted file mode 100644 index 4cbf9e4a4b7..00000000000 --- a/apps/files_encryption/lib/stream.php +++ /dev/null @@ -1,700 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Florin Peter <github@florin-peter.de> - * @author jknockaert <jasper@knockaert.nl> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <rmccorkell@karoshi.org.uk> - * @author Sam Tuke <mail@samtuke.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -/** - * transparently encrypted filestream - * - * you can use it as wrapper around an existing stream by setting CryptStream::$sourceStreams['foo']=array('path'=>$path,'stream'=>$stream) - * and then fopen('crypt://streams/foo'); - */ - -namespace OCA\Files_Encryption; - -use OCA\Files_Encryption\Exception\EncryptionException; - -/** - * Provides 'crypt://' stream wrapper protocol. - * @note We use a stream wrapper because it is the most secure way to handle - * decrypted content transfers. There is no safe way to decrypt the entire file - * somewhere on the server, so we have to encrypt and decrypt blocks on the fly. - * @note Paths used with this protocol MUST BE RELATIVE. Use URLs like: - * crypt://filename, or crypt://subdirectory/filename, NOT - * crypt:///home/user/owncloud/data. Otherwise keyfiles will be put in - * [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and - * will not be accessible to other methods. - * @note Data read and written must always be 8192 bytes long, as this is the - * buffer size used internally by PHP. The encryption process makes the input - * data longer, and input is chunked into smaller pieces in order to result in - * a 8192 encrypted block size. - * @note When files are deleted via webdav, or when they are updated and the - * previous version deleted, this is handled by OC\Files\View, and thus the - * encryption proxies are used and keyfiles deleted. - */ -class Stream { - - const PADDING_CHAR = '-'; - - private $plainKey; - private $encKeyfiles; - private $rawPath; // The raw path relative to the data dir - private $relPath; // rel path to users file dir - private $userId; - 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 $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 $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 - */ - private $rootView; // a fsview object set to '/' - - /** - * @var \OCA\Files_Encryption\Session - */ - private $session; - private $privateKey; - - /** - * @param string $path raw path relative to data/ - * @param string $mode - * @param int $options - * @param string $opened_path - * @return bool - * @throw \OCA\Files_Encryption\Exception\EncryptionException - */ - public function stream_open($path, $mode, $options, &$opened_path) { - - // read default cipher from config - $this->cipher = Helper::getCipher(); - - // assume that the file already exist before we decide it finally in getKey() - $this->newFile = false; - - $this->rootView = new \OC\Files\View('/'); - - $this->session = new Session($this->rootView); - - $this->privateKey = $this->session->getPrivateKey(); - if ($this->privateKey === false) { - throw new EncryptionException('Session does not contain a private key, maybe your login password changed?', - EncryptionException::PRIVATE_KEY_MISSING); - } - - $normalizedPath = \OC\Files\Filesystem::normalizePath(str_replace('crypt://', '', $path)); - $originalFile = Helper::getPathFromTmpFile($normalizedPath); - if ($originalFile) { - $this->rawPath = $originalFile; - $this->isLocalTmpFile = true; - $this->localTmpFile = $normalizedPath; - } else { - $this->rawPath = $normalizedPath; - } - - $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 = $this->util->getKeyId(); - - $fileType = Helper::detectFileType($this->rawPath); - - 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 { - $this->size = $this->rootView->filesize($this->rawPath); - \OC_FileProxy::$enabled = true; - $this->unencryptedSize = $this->rootView->filesize($this->rawPath); - \OC_FileProxy::$enabled = false; - $this->readHeader(); - } - - \OC_FileProxy::$enabled = $proxyStatus; - - if (!is_resource($this->handle)) { - - \OCP\Util::writeLog('Encryption library', 'failed to open file "' . $this->rawPath . '"', \OCP\Util::ERROR); - - } else { - - $this->meta = stream_get_meta_data($this->handle); - // sometimes fopen changes the mode, e.g. for a url "r" convert to "r+" - // but we need to remember the original access type - $this->meta['mode'] = $mode; - - } - - - return is_resource($this->handle); - - } - - private function readHeader() { - - if (is_resource($this->handle)) { - $data = fread($this->handle, Crypt::BLOCKSIZE); - - $header = Crypt::parseHeader($data); - $this->cipher = Crypt::getCipher($header); - - // 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); - } - } - - } - } - - /** - * Returns the current position of the file pointer - * @return int position of the file pointer - */ - public function stream_tell() { - return $this->position; - } - - /** - * @param int $offset - * @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 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; - - } - - /** - * @param int $count - * @return bool|string - * @throws \OCA\Files_Encryption\Exception\EncryptionException - */ - 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); - - // loop over the 6126 sized unencrypted blocks - while ($count > 0) { - - $remainingLength = $count; - - // 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 { - $result .= substr($this->cache,$blockPosition); - $this->flush(); - $this->position += (6126 - $blockPosition); - $count -= (6126 - $blockPosition); - } - - } - - return $result; - - } - - /** - * Encrypt and pad data ready for writing to disk - * @param string $plainData data to be encrypted - * @param string $key key to use for encryption - * @return string encrypted data on success, false on failure - */ - public function preWriteEncrypt($plainData, $key) { - - // Encrypt data to 'catfile', which includes IV - if ($encrypted = Crypt::symmetricEncryptFileContent($plainData, $key, $this->cipher)) { - - return $encrypted; - - } else { - - return false; - - } - - } - - /** - * Fetch the plain encryption key for the file and set it as plainKey property - * @internal param bool $generate if true, a new key will be generated if none can be found - * @return bool true on key found and set, false on key not found and new key generated and set - */ - public function getKey() { - - // Check if key is already set - if (isset($this->plainKey) && isset($this->encKeyfile)) { - - return true; - - } - - // Fetch and decrypt keyfile - // Fetch existing keyfile - $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, $this->util, $this->relPath); - - // if there is no valid private key return false - if ($this->privateKey === false) { - // if private key is not valid redirect user to a error page - Helper::redirectToErrorPage($this->session); - return false; - } - - if ($shareKey === false) { - // if no share key is available redirect user to a error page - Helper::redirectToErrorPage($this->session, Crypt::ENCRYPTION_NO_SHARE_KEY_FOUND); - return false; - } - - $this->plainKey = Crypt::multiKeyDecrypt($this->encKeyfile, $shareKey, $this->privateKey); - - return true; - - } else { - - $this->newFile = true; - - return false; - - } - - } - - /** - * write header at beginning of encrypted file - * - * @throws \OCA\Files_Encryption\Exception\EncryptionException - */ - private function writeHeader() { - - $header = Crypt::generateHeader(); - - if (strlen($header) > Crypt::BLOCKSIZE) { - throw new EncryptionException('max header size exceeded', EncryptionException::ENCRYPTION_HEADER_TO_LARGE); - } - - $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; - } - - /** - * Handle plain data from the stream, and write it in 8192 byte blocks - * @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 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 - */ - public function stream_write($data) { - - // if there is no valid private key return false - if ($this->privateKey === false) { - $this->size = 0; - return strlen($data); - } - - if ($this->size === 0) { - $this->writeHeader(); - } - - // 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; - - // loop over $data to fit it in 6126 sized unencrypted blocks - while (strlen($data) > 0) { - - $remainingLength = strlen($data); - - // set the cache to the current 6126 block - $this->readCache(); - - // 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)) { - - // 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 { - $data=''; - } - } - - $this->unencryptedSize = max($this->unencryptedSize,$this->position); - - return $length; - - } - - - /** - * @param int $option - * @param int $arg1 - * @param int|null $arg2 - */ - public function stream_set_option($option, $arg1, $arg2) { - $return = false; - switch ($option) { - case STREAM_OPTION_BLOCKING: - $return = stream_set_blocking($this->handle, $arg1); - break; - case STREAM_OPTION_READ_TIMEOUT: - $return = stream_set_timeout($this->handle, $arg1, $arg2); - break; - case STREAM_OPTION_WRITE_BUFFER: - $return = stream_set_write_buffer($this->handle, $arg1); - } - - return $return; - } - - /** - * @return array - */ - public function stream_stat() { - return fstat($this->handle); - } - - /** - * @param int $mode - */ - public function stream_lock($mode) { - return flock($this->handle, $mode); - } - - /** - * @return bool - */ - public function stream_flush() { - - $this->flush(); - return fflush($this->handle); - // Not a typo: http://php.net/manual/en/function.fflush.php - - } - - /** - * @return bool - */ - public function stream_eof() { - return ($this->position>=$this->unencryptedSize); - } - - 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; - // 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; - } - } - - /** - * @return bool - */ - public function stream_close() { - - $this->flush(); - - // if there is no valid private key return false - if ($this->privateKey === false) { - - // cleanup - if ($this->meta['mode'] !== 'r' && $this->meta['mode'] !== 'rb' && !$this->isLocalTmpFile) { - - // Disable encryption proxy to prevent recursive calls - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; - - if ($this->rootView->file_exists($this->rawPath) && $this->size === $this->headerSize) { - fclose($this->handle); - $this->rootView->unlink($this->rawPath); - } - - // Re-enable proxy - our work is done - \OC_FileProxy::$enabled = $proxyStatus; - } - - // if private key is not valid redirect user to a error page - Helper::redirectToErrorPage($this->session); - } - - if ( - $this->meta['mode'] !== 'r' && - $this->meta['mode'] !== 'rb' && - $this->isLocalTmpFile === false && - $this->size > $this->headerSize && - $this->unencryptedSize > 0 - ) { - - // only write keyfiles if it was a new file - if ($this->newFile === true) { - - // Disable encryption proxy to prevent recursive calls - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; - - // Fetch user's public key - $this->publicKey = Keymanager::getPublicKey($this->rootView, $this->keyId); - - // Check if OC sharing api is enabled - $sharingEnabled = \OCP\Share::isEnabled(); - - // Get all users sharing the file includes current user - $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']); - - // Encrypt enc key for all sharing users - $this->encKeyfiles = Crypt::multiKeyEncrypt($this->plainKey, $publicKeys); - - // Save the new encrypted file key - Keymanager::setFileKey($this->rootView, $this->util, $this->relPath, $this->encKeyfiles['data']); - - // Save the sharekeys - Keymanager::setShareKeys($this->rootView, $this->util, $this->relPath, $this->encKeyfiles['keys']); - - // Re-enable proxy - our work is done - \OC_FileProxy::$enabled = $proxyStatus; - } - - // we need to update the file info for the real file, not for the - // part file. - $path = Helper::stripPartialFileExtension($this->rawPath); - - $fileInfo = array( - 'mimetype' => $this->rootView->getMimeType($this->rawPath), - 'encrypted' => true, - 'unencrypted_size' => $this->unencryptedSize, - ); - - // if we write a part file we also store the unencrypted size for - // the part file so that it can be re-used later - $this->rootView->putFileInfo($this->rawPath, $fileInfo); - if ($path !== $this->rawPath) { - $this->rootView->putFileInfo($path, $fileInfo); - } - - } - - $result = fclose($this->handle); - - if ($result === false) { - \OCP\Util::writeLog('Encryption library', 'Could not close stream, file could be corrupted', \OCP\Util::FATAL); - } - - return $result; - - } - -} |