/** * store a new version of a file. */ public function store($filename) { if (\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED) == 'true') { list($uid, $filename) = self::getUidAndFilename($filename); $files_view = new \OC_FilesystemView('/' . $uid . '/files'); $users_view = new \OC_FilesystemView('/' . $uid); //check if source file already exist as version to avoid recursions. // todo does this check work? if ($users_view->file_exists($filename)) { return false; } // check if filename is a directory if ($files_view->is_dir($filename)) { return false; } // check filetype blacklist $blacklist = explode(' ', \OCP\Config::getSystemValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST)); foreach ($blacklist as $bl) { $parts = explode('.', $filename); $ext = end($parts); if (strtolower($ext) == $bl) { return false; } } // we should have a source file to work with if (!$files_view->file_exists($filename)) { return false; } // check filesize if ($files_view->filesize($filename) > \OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)) { return false; } // check mininterval if the file is being modified by the owner (all shared files should be versioned despite mininterval) if ($uid == \OCP\User::getUser()) { $versions_fileview = new \OC_FilesystemView('/' . $uid . '/files_versions'); $versionsName = \OCP\Config::getSystemValue('datadirectory') . $versions_fileview->getAbsolutePath($filename); $versionsFolderName = \OCP\Config::getSystemValue('datadirectory') . $versions_fileview->getAbsolutePath(''); $matches = glob($versionsName . '.v*'); sort($matches); $parts = explode('.v', end($matches)); if (end($parts) + Storage::DEFAULTMININTERVAL > time()) { return false; } } // create all parent folders $info = pathinfo($filename); if (!file_exists($versionsFolderName . '/' . $info['dirname'])) { mkdir($versionsFolderName . '/' . $info['dirname'], 0750, true); } // store a new version of a file $users_view->copy('files' . $filename, 'files_versions' . $filename . '.v' . time()); // expire old revisions if necessary Storage::expire($filename); } }
function testDescryptAllWithBrokenFiles() { $file1 = "/decryptAll1" . uniqid() . ".txt"; $file2 = "/decryptAll2" . uniqid() . ".txt"; $util = new Encryption\Util($this->view, $this->userId); $this->view->file_put_contents($this->userId . '/files/' . $file1, $this->dataShort); $this->view->file_put_contents($this->userId . '/files/' . $file2, $this->dataShort); $fileInfoEncrypted1 = $this->view->getFileInfo($this->userId . '/files/' . $file1); $fileInfoEncrypted2 = $this->view->getFileInfo($this->userId . '/files/' . $file2); $this->assertTrue(is_array($fileInfoEncrypted1)); $this->assertTrue(is_array($fileInfoEncrypted2)); $this->assertEquals($fileInfoEncrypted1['encrypted'], 1); $this->assertEquals($fileInfoEncrypted2['encrypted'], 1); // rename keyfile for file1 so that the decryption for file1 fails // Expected behaviour: decryptAll() returns false, file2 gets decrypted anyway $this->view->rename($this->userId . '/files_encryption/keyfiles/' . $file1 . '.key', $this->userId . '/files_encryption/keyfiles/' . $file1 . '.key.moved'); // decrypt all encrypted files $result = $util->decryptAll('/' . $this->userId . '/' . 'files'); $this->assertFalse($result); $fileInfoUnencrypted1 = $this->view->getFileInfo($this->userId . '/files/' . $file1); $fileInfoUnencrypted2 = $this->view->getFileInfo($this->userId . '/files/' . $file2); $this->assertTrue(is_array($fileInfoUnencrypted1)); $this->assertTrue(is_array($fileInfoUnencrypted2)); // file1 should be still encrypted; file2 should be decrypted $this->assertEquals(1, $fileInfoUnencrypted1['encrypted']); $this->assertEquals(0, $fileInfoUnencrypted2['encrypted']); // keyfiles and share keys should still exist $this->assertTrue($this->view->is_dir($this->userId . '/files_encryption/keyfiles/')); $this->assertTrue($this->view->is_dir($this->userId . '/files_encryption/share-keys/')); // rename the keyfile for file1 back $this->view->rename($this->userId . '/files_encryption/keyfiles/' . $file1 . '.key.moved', $this->userId . '/files_encryption/keyfiles/' . $file1 . '.key'); // try again to decrypt all encrypted files $result = $util->decryptAll('/' . $this->userId . '/' . 'files'); $this->assertTrue($result); $fileInfoUnencrypted1 = $this->view->getFileInfo($this->userId . '/files/' . $file1); $fileInfoUnencrypted2 = $this->view->getFileInfo($this->userId . '/files/' . $file2); $this->assertTrue(is_array($fileInfoUnencrypted1)); $this->assertTrue(is_array($fileInfoUnencrypted2)); // now both files should be decrypted $this->assertEquals(0, $fileInfoUnencrypted1['encrypted']); $this->assertEquals(0, $fileInfoUnencrypted2['encrypted']); // keyfiles and share keys should be deleted $this->assertFalse($this->view->is_dir($this->userId . '/files_encryption/keyfiles/')); $this->assertFalse($this->view->is_dir($this->userId . '/files_encryption/share-keys/')); $this->view->unlink($this->userId . '/files/' . $file1); $this->view->unlink($this->userId . '/files/' . $file2); }
/** * @brief if session is started, check if ownCloud key pair is set up, if not create it * @param \OC_FilesystemView $view * * @note The ownCloud key pair is used to allow public link sharing even if encryption is enabled */ public function __construct($view) { $this->view = $view; if (!$this->view->is_dir('owncloud_private_key')) { $this->view->mkdir('owncloud_private_key'); } $publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId'); if ($publicShareKeyId === null) { $publicShareKeyId = 'pubShare_' . substr(md5(time()), 0, 8); \OC_Appconfig::setValue('files_encryption', 'publicShareKeyId', $publicShareKeyId); } if (!$this->view->file_exists("/public-keys/" . $publicShareKeyId . ".public.key") || !$this->view->file_exists("/owncloud_private_key/" . $publicShareKeyId . ".private.key")) { $keypair = Crypt::createKeypair(); // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; // Save public key if (!$view->is_dir('/public-keys')) { $view->mkdir('/public-keys'); } $this->view->file_put_contents('/public-keys/' . $publicShareKeyId . '.public.key', $keypair['publicKey']); // Encrypt private key empty passphrase $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], ''); // Save private key $this->view->file_put_contents('/owncloud_private_key/' . $publicShareKeyId . '.private.key', $encryptedPrivateKey); \OC_FileProxy::$enabled = $proxyStatus; } if (\OCA\Encryption\Helper::isPublicAccess()) { // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $encryptedKey = $this->view->file_get_contents('/owncloud_private_key/' . $publicShareKeyId . '.private.key'); $privateKey = Crypt::decryptPrivateKey($encryptedKey, ''); $this->setPublicSharePrivateKey($privateKey); \OC_FileProxy::$enabled = $proxyStatus; } }
/** * @brief mark file as renamed so that we know the original source after the file was renamed * @param array $params with the old path and the new path */ public static function preRename($params) { $user = \OCP\User::getUser(); $view = new \OC_FilesystemView('/'); $util = new Util($view, $user); list($ownerOld, $pathOld) = $util->getUidAndFilename($params['oldpath']); // we only need to rename the keys if the rename happens on the same mountpoint // otherwise we perform a stream copy, so we get a new set of keys $mp1 = $view->getMountPoint('/' . $user . '/files/' . $params['oldpath']); $mp2 = $view->getMountPoint('/' . $user . '/files/' . $params['newpath']); $type = $view->is_dir('/' . $user . '/files/' . $params['oldpath']) ? 'folder' : 'file'; if ($mp1 === $mp2) { self::$renamedFiles[$params['oldpath']] = array('uid' => $ownerOld, 'path' => $pathOld, 'type' => $type, 'operation' => 'rename'); } }
/** * @param $path * @param $size * @return bool */ public function postFileSize($path, $size, $fileInfo = null) { $view = new \OC_FilesystemView('/'); $userId = Helper::getUser($path); $util = new Util($view, $userId); // if encryption is no longer enabled or if the files aren't migrated yet // we return the default file size if (!\OCP\App::isEnabled('files_encryption') || $util->getMigrationStatus() !== Util::MIGRATION_COMPLETED) { return $size; } // if path is a folder do nothing if ($view->is_dir($path)) { $proxyState = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $fileInfo = $view->getFileInfo($path); \OC_FileProxy::$enabled = $proxyState; if (isset($fileInfo['unencrypted_size']) && $fileInfo['unencrypted_size'] > 0) { return $fileInfo['unencrypted_size']; } return $size; } // get relative path $relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path); // if path is empty we cannot resolve anything if (empty($relativePath)) { return $size; } // get file info from database/cache if not .part file if (empty($fileInfo) && !Helper::isPartialFilePath($path)) { $proxyState = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $fileInfo = $view->getFileInfo($path); \OC_FileProxy::$enabled = $proxyState; } // if file is encrypted return real file size if (isset($fileInfo['encrypted']) && $fileInfo['encrypted'] === true) { // try to fix unencrypted file size if it doesn't look plausible if ((int) $fileInfo['size'] > 0 && (int) $fileInfo['unencrypted_size'] === 0) { $fixSize = $util->getFileSize($path); $fileInfo['unencrypted_size'] = $fixSize; // put file info if not .part file if (!Helper::isPartialFilePath($relativePath)) { $view->putFileInfo($path, $fileInfo); } } $size = $fileInfo['unencrypted_size']; } else { // self healing if file was removed from file cache if (!is_array($fileInfo)) { $fileInfo = array(); } $fixSize = $util->getFileSize($path); if ($fixSize > 0) { $size = $fixSize; $fileInfo['encrypted'] = true; $fileInfo['unencrypted_size'] = $size; // put file info if not .part file if (!Helper::isPartialFilePath($relativePath)) { $view->putFileInfo($path, $fileInfo); } } } return $size; }
/** * Share an item, adds an entry into the database * @param $source The source location of the item * @param $uid_shared_with The user or group to share the item with * @param $permissions The permissions, use the constants WRITE and DELETE */ public function __construct($source, $uid_shared_with, $permissions) { $uid_owner = OCP\USER::getUser(); $query = OCP\DB::prepare("INSERT INTO *PREFIX*sharing VALUES(?,?,?,?,?)"); // Check if this is a reshare and use the original source if ($result = OC_Share::getSource($source)) { $source = $result; } if ($uid_shared_with == self::PUBLICLINK) { $token = sha1("{$uid_shared_with}-{$source}"); $query->execute(array($uid_owner, self::PUBLICLINK, $source, $token, $permissions)); $this->token = $token; } else { if (OC_Group::groupExists($uid_shared_with)) { $gid = $uid_shared_with; $uid_shared_with = OC_Group::usersInGroup($gid); // Remove the owner from the list of users in the group $uid_shared_with = array_diff($uid_shared_with, array($uid_owner)); } else { if (OCP\User::userExists($uid_shared_with)) { $userGroups = OC_Group::getUserGroups($uid_owner); // Check if the user is in one of the owner's groups foreach ($userGroups as $group) { if ($inGroup = OC_Group::inGroup($uid_shared_with, $group)) { $gid = null; $uid_shared_with = array($uid_shared_with); break; } } if (!$inGroup) { throw new Exception("You can't share with " . $uid_shared_with); } } else { throw new Exception($uid_shared_with . " is not a user"); } } foreach ($uid_shared_with as $uid) { // Check if this item is already shared with the user $checkSource = OCP\DB::prepare("SELECT source FROM *PREFIX*sharing WHERE source = ? AND uid_shared_with " . self::getUsersAndGroups($uid, false)); $resultCheckSource = $checkSource->execute(array($source))->fetchAll(); // TODO Check if the source is inside a folder if (count($resultCheckSource) > 0) { if (!isset($gid)) { throw new Exception("This item is already shared with " . $uid); } else { // Skip this user if sharing with a group continue; } } // Check if the target already exists for the user, if it does append a number to the name $sharedFolder = '/' . $uid . '/files/Shared'; $target = $sharedFolder . "/" . basename($source); $checkTarget = OCP\DB::prepare("SELECT source FROM *PREFIX*sharing WHERE target = ? AND uid_shared_with " . self::getUsersAndGroups($uid, false) . " LIMIT 1"); $result = $checkTarget->execute(array($target))->fetchAll(); if (count($result) > 0) { if ($pos = strrpos($target, ".")) { $name = substr($target, 0, $pos); $ext = substr($target, $pos); } else { $name = $target; $ext = ""; } $counter = 1; while (count($result) > 0) { $target = $name . "_" . $counter . $ext; $result = $checkTarget->execute(array($target))->fetchAll(); $counter++; } } if (isset($gid)) { $uid = $uid . "@" . $gid; } $query->execute(array($uid_owner, $uid, $source, $target, $permissions)); // Update mtime of shared folder to invoke a file cache rescan $rootView = new OC_FilesystemView('/'); if (!$rootView->is_dir($sharedFolder)) { if (!$rootView->is_dir('/' . $uid . '/files')) { OC_Util::tearDownFS(); OC_Util::setupFS($uid); OC_Util::tearDownFS(); } $rootView->mkdir($sharedFolder); } $rootView->touch($sharedFolder); } } }
/** * update the filecache according to changes to the filesystem * @param string path * @param string root (optional) */ public static function update($path, $root = false) { if ($root === false) { $view = OC_Filesystem::getView(); } else { $view = new OC_FilesystemView($root); } $mimetype = $view->getMimeType($path); $size = 0; $cached = OC_FileCache_Cached::get($path, $root); $cachedSize = isset($cached['size']) ? $cached['size'] : 0; if ($view->is_dir($path . '/')) { if (OC_FileCache::inCache($path, $root)) { $cachedContent = OC_FileCache_Cached::getFolderContent($path, $root); foreach ($cachedContent as $file) { $size += $file['size']; } $mtime = $view->filemtime($path . '/'); $ctime = $view->filectime($path . '/'); $writable = $view->is_writable($path . '/'); OC_FileCache::put($path, array('size' => $size, 'mtime' => $mtime, 'ctime' => $ctime, 'mimetype' => $mimetype, 'writable' => $writable)); } else { $count = 0; OC_FileCache::scan($path, null, $count, $root); return; //increaseSize is already called inside scan } } else { $size = OC_FileCache::scanFile($path, $root); } OC_FileCache::increaseSize(dirname($path), $size - $cachedSize, $root); }
/** * @param $path * @param $size * @return bool */ public function postFileSize($path, $size) { $view = new \OC_FilesystemView('/'); // if path is a folder do nothing if ($view->is_dir($path)) { return $size; } // get relative path $relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path); // if path is empty we cannot resolve anything if (empty($relativePath)) { return $size; } $fileInfo = false; // get file info from database/cache if not .part file if (!Keymanager::isPartialFilePath($path)) { $fileInfo = $view->getFileInfo($path); } // if file is encrypted return real file size if (is_array($fileInfo) && $fileInfo['encrypted'] === true) { $size = $fileInfo['unencrypted_size']; } else { // self healing if file was removed from file cache if (!is_array($fileInfo)) { $fileInfo = array(); } $userId = \OCP\User::getUser(); $util = new Util($view, $userId); $fixSize = $util->getFileSize($path); if ($fixSize > 0) { $size = $fixSize; $fileInfo['encrypted'] = true; $fileInfo['unencrypted_size'] = $size; // put file info if not .part file if (!Keymanager::isPartialFilePath($relativePath)) { $view->putFileInfo($path, $fileInfo); } } } return $size; }
/** * recursively scan the filesystem and fill the cache * @param string $path * @param OC_EventSource $enventSource (optional) * @param int count (optional) * @param string root (optional) */ public static function scan($path, $eventSource = false, &$count = 0, $root = false) { if ($eventSource) { $eventSource->send('scanning', array('file' => $path, 'count' => $count)); } $lastSend = $count; // NOTE: Ugly hack to prevent shared files from going into the cache (the source already exists somewhere in the cache) if (substr($path, 0, 7) == '/Shared') { return; } if ($root === false) { $view = OC_Filesystem::getView(); } else { $view = new OC_FilesystemView($root); } self::scanFile($path, $root); $dh = $view->opendir($path . '/'); $totalSize = 0; if ($dh) { while (($filename = readdir($dh)) !== false) { if ($filename != '.' and $filename != '..') { $file = $path . '/' . $filename; if ($view->is_dir($file . '/')) { self::scan($file, $eventSource, $count, $root); } else { $totalSize += self::scanFile($file, $root); $count++; if ($count > $lastSend + 25 and $eventSource) { $lastSend = $count; $eventSource->send('scanning', array('file' => $path, 'count' => $count)); } } } } } OC_FileCache_Update::cleanFolder($path, $root); self::increaseSize($path, $totalSize, $root); }
/** * @brief after a file is renamed, rename its keyfile and share-keys also fix the file size and fix also the sharing * @param array with oldpath and newpath * * This function is connected to the rename signal of OC_Filesystem and adjust the name and location * of the stored versions along the actual file */ public static function postRename($params) { if (\OCP\App::isEnabled('files_encryption') === false) { return true; } // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $view = new \OC_FilesystemView('/'); $session = new \OCA\Encryption\Session($view); $userId = \OCP\User::getUser(); $util = new Util($view, $userId); // Format paths to be relative to user files dir if ($util->isSystemWideMountPoint($params['oldpath'])) { $baseDir = 'files_encryption/'; $oldKeyfilePath = $baseDir . 'keyfiles/' . $params['oldpath']; } else { $baseDir = $userId . '/' . 'files_encryption/'; $oldKeyfilePath = $baseDir . 'keyfiles/' . $params['oldpath']; } if ($util->isSystemWideMountPoint($params['newpath'])) { $newKeyfilePath = $baseDir . 'keyfiles/' . $params['newpath']; } else { $newKeyfilePath = $baseDir . 'keyfiles/' . $params['newpath']; } // add key ext if this is not an folder if (!$view->is_dir($oldKeyfilePath)) { $oldKeyfilePath .= '.key'; $newKeyfilePath .= '.key'; // handle share-keys $localKeyPath = $view->getLocalFile($baseDir . 'share-keys/' . $params['oldpath']); $escapedPath = Helper::escapeGlobPattern($localKeyPath); $matches = glob($escapedPath . '*.shareKey'); foreach ($matches as $src) { $dst = \OC\Files\Filesystem::normalizePath(str_replace($params['oldpath'], $params['newpath'], $src)); // create destination folder if not exists if (!file_exists(dirname($dst))) { mkdir(dirname($dst), 0750, true); } rename($src, $dst); } } else { // handle share-keys folders $oldShareKeyfilePath = $baseDir . 'share-keys/' . $params['oldpath']; $newShareKeyfilePath = $baseDir . 'share-keys/' . $params['newpath']; // create destination folder if not exists if (!$view->file_exists(dirname($newShareKeyfilePath))) { $view->mkdir(dirname($newShareKeyfilePath), 0750, true); } $view->rename($oldShareKeyfilePath, $newShareKeyfilePath); } // Rename keyfile so it isn't orphaned if ($view->file_exists($oldKeyfilePath)) { // create destination folder if not exists if (!$view->file_exists(dirname($newKeyfilePath))) { $view->mkdir(dirname($newKeyfilePath), 0750, true); } $view->rename($oldKeyfilePath, $newKeyfilePath); } // build the path to the file $newPath = '/' . $userId . '/files' . $params['newpath']; $newPathRelative = $params['newpath']; if ($util->fixFileSize($newPath)) { // get sharing app state $sharingEnabled = \OCP\Share::isEnabled(); // get users $usersSharing = $util->getSharingUsersArray($sharingEnabled, $newPathRelative); // update sharing-keys $util->setSharedFileKeyfiles($session, $usersSharing, $newPathRelative); } \OC_FileProxy::$enabled = $proxyStatus; }
/** * check if a file or folder is updated outside owncloud * @param string path * @param string root (optional) * @return bool */ public static function isUpdated($path, $root = '') { if (!$root) { $root = OC_Filesystem::getRoot(); $view = OC_Filesystem::getView(); } else { if ($root == '/') { $root = ''; } $view = new OC_FilesystemView($root); } if (!$view->file_exists($path)) { return false; } $mtime = $view->filemtime($path); $isDir = $view->is_dir($path); $fullPath = $root . $path; $query = OC_DB::prepare('SELECT mtime FROM *PREFIX*fscache WHERE path_hash=?'); $result = $query->execute(array(md5($fullPath))); if ($row = $result->fetchRow()) { $cachedMTime = $row['mtime']; return $mtime > $cachedMTime; } else { //file not in cache, so it has to be updated if ($path == '/' or $path == '') { //dont auto update the root folder, it will be scanned return false; } return true; } }
/** * @brief Make preparations to vars and filesystem for saving a keyfile */ public static function keySetPreparation(\OC_FilesystemView $view, $path, $basePath, $userId) { $targetPath = ltrim($path, '/'); $path_parts = pathinfo($targetPath); // If the file resides within a subdirectory, create it if (isset($path_parts['dirname']) && !$view->file_exists($basePath . '/' . $path_parts['dirname'])) { $sub_dirs = explode(DIRECTORY_SEPARATOR, $basePath . '/' . $path_parts['dirname']); $dir = ''; foreach ($sub_dirs as $sub_dir) { $dir .= '/' . $sub_dir; if (!$view->is_dir($dir)) { $view->mkdir($dir); } } } return $targetPath; }