, * @author Frank Karlitschek , * @author Bjoern Schiessle * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE * License as published by the Free Software Foundation; either * version 3 of the License, or any later version. * * This library 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 along with this library. If not, see . * */ namespace OCA\Files_Encryption; /** * Class for utilities relating to encrypted file storage system * @param \OC\Files\View $view expected to have OC '/' as root path * @param string $userId ID of the logged in user * @param int $client indicating status of client side encryption. Currently * unused, likely to become obsolete shortly */ class Util { const MIGRATION_COMPLETED = 1; // migration to new encryption completed const MIGRATION_IN_PROGRESS = -1; // migration is running const MIGRATION_OPEN = 0; // user still needs to be migrated private $view; // OC\Files\View object for filesystem operations private $userId; // ID of the user we use to encrypt/decrypt files private $keyId; // ID of the key we want to manipulate private $client; // Client side encryption mode flag private $publicKeyDir; // Dir containing all public user keys private $encryptionDir; // Dir containing user's files_encryption private $keysPath; // Dir containing all file related encryption keys private $publicKeyPath; // Path to user's public key private $privateKeyPath; // Path to user's private key private $userFilesDir; private $publicShareKeyId; private $recoveryKeyId; private $isPublic; /** * @param \OC\Files\View $view * @param string $userId * @param bool $client */ public function __construct($view, $userId, $client = false) { $this->view = $view; $this->client = $client; $this->userId = $userId; $appConfig = \OC::$server->getAppConfig(); $this->publicShareKeyId = $appConfig->getValue('files_encryption', 'publicShareKeyId'); $this->recoveryKeyId = $appConfig->getValue('files_encryption', 'recoveryKeyId'); $this->userDir = '/' . $this->userId; $this->fileFolderName = 'files'; $this->userFilesDir = '/' . $userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? $this->publicKeyDir = Keymanager::getPublicKeyPath(); $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; $this->keysPath = $this->encryptionDir . '/' . 'keys'; $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.publicKey'; // e.g. data/public-keys/admin.publicKey $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.privateKey'; // e.g. data/admin/admin.privateKey // make sure that the owners home is mounted \OC\Files\Filesystem::initMountPoints($userId); if (Helper::isPublicAccess()) { $this->keyId = $this->publicShareKeyId; $this->isPublic = true; } else { $this->keyId = $this->userId; $this->isPublic = false; } } /** * @return bool */ public function ready() { if ( !$this->view->file_exists($this->encryptionDir) or !$this->view->file_exists($this->keysPath) or !$this->view->file_exists($this->publicKeyPath) or !$this->view->file_exists($this->privateKeyPath) ) { return false; } else { return true; } } /** * check if the users private & public key exists * @return boolean */ public function userKeysExists() { if ( $this->view->file_exists($this->privateKeyPath) && $this->view->file_exists($this->publicKeyPath)) { return true; } else { return false; } } /** * create a new public/private key pair for the user * * @param string $password password for the private key */ public function replaceUserKeys($password) { $this->backupAllKeys('password_reset'); $this->view->unlink($this->publicKeyPath); $this->view->unlink($this->privateKeyPath); $this->setupServerSide($password); } /** * Sets up user folders and keys for serverside encryption * * @param string $passphrase to encrypt server-stored private key with * @return bool */ public function setupServerSide($passphrase = null) { // Set directories to check / create $setUpDirs = array( $this->userDir, $this->publicKeyDir, $this->encryptionDir, $this->keysPath ); // Check / create all necessary dirs foreach ($setUpDirs as $dirPath) { if (!$this->view->file_exists($dirPath)) { $this->view->mkdir($dirPath); } } // Create user keypair // we should never override a keyfile if ( !$this->view->file_exists($this->publicKeyPath) && !$this->view->file_exists($this->privateKeyPath) ) { // Generate keypair $keypair = Crypt::createKeypair(); if ($keypair) { \OC_FileProxy::$enabled = false; // Encrypt private key with user pwd as passphrase $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase, Helper::getCipher()); // Save key-pair if ($encryptedPrivateKey) { $header = crypt::generateHeader(); $this->view->file_put_contents($this->privateKeyPath, $header . $encryptedPrivateKey); $this->view->file_put_contents($this->publicKeyPath, $keypair['publicKey']); } \OC_FileProxy::$enabled = true; } } else { // check if public-key exists but private-key is missing if ($this->view->file_exists($this->publicKeyPath) && !$this->view->file_exists($this->privateKeyPath)) { \OCP\Util::writeLog('Encryption library', 'public key exists but private key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); return false; } else { if (!$this->view->file_exists($this->publicKeyPath) && $this->view->file_exists($this->privateKeyPath) ) { \OCP\Util::writeLog('Encryption library', 'private key exists but public key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); return false; } } } return true; } /** * @return string */ public function getPublicShareKeyId() { return $this->publicShareKeyId; } /** * Check whether pwd recovery is enabled for a given user * @return bool 1 = yes, 0 = no, false = no record * * @note If records are not being returned, check for a hidden space * at the start of the uid in db */ public function recoveryEnabledForUser() { $recoveryMode = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'recovery_enabled', '0'); return ($recoveryMode === '1') ? true : false; } /** * Enable / disable pwd recovery for a given user * @param bool $enabled Whether to enable or disable recovery * @return bool */ public function setRecoveryForUser($enabled) { $value = $enabled ? '1' : '0'; try { \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'recovery_enabled', $value); return true; } catch(\OCP\PreConditionNotMetException $e) { return false; } } /** * Find all files and their encryption status within a directory * @param string $directory The path of the parent directory to search * @param bool $found the founded files if called again * @return array keys: plain, encrypted, broken * @note $directory needs to be a path relative to OC data dir. e.g. * /admin/files NOT /backup OR /home/www/oc/data/admin/files */ public function findEncFiles($directory, &$found = false) { // Disable proxy - we don't want files to be decrypted before // we handle them \OC_FileProxy::$enabled = false; if ($found === false) { $found = array( 'plain' => array(), 'encrypted' => array(), 'broken' => array(), ); } if ($this->view->is_dir($directory) && $handle = $this->view->opendir($directory)){ if (is_resource($handle)) { while (false !== ($file = readdir($handle))) { if ($file !== "." && $file !== "..") { $filePath = $directory . '/' . $this->view->getRelativePath('/' . $file); $relPath = Helper::stripUserFilesPath($filePath); // If the path is a directory, search // its contents if ($this->view->is_dir($filePath)) { $this->findEncFiles($filePath, $found); // If the path is a file, determine // its encryption status } elseif ($this->view->is_file($filePath)) { // Disable proxies again, some- // where they got re-enabled :/ \OC_FileProxy::$enabled = false; $isEncryptedPath = $this->isEncryptedPath($filePath); // If the file is encrypted // NOTE: If the userId is // empty or not set, file will // detected as plain // NOTE: This is inefficient; // scanning every file like this // will eat server resources :( if ($isEncryptedPath) { $fileKey = Keymanager::getFileKey($this->view, $this, $relPath); $shareKey = Keymanager::getShareKey($this->view, $this->userId, $this, $relPath); // if file is encrypted but now file key is available, throw exception if ($fileKey === false || $shareKey === false) { \OCP\Util::writeLog('encryption library', 'No keys available to decrypt the file: ' . $filePath, \OCP\Util::ERROR); $found['broken'][] = array( 'name' => $file, 'path' => $filePath, ); } else { $found['encrypted'][] = array( 'name' => $file, 'path' => $filePath, ); } // If the file is not encrypted } else { $found['plain'][] = array( 'name' => $file, 'path' => $relPath ); } } } } } } \OC_FileProxy::$enabled = true; return $found; } /** * Check if a given path identifies an encrypted file * @param string $path * @return boolean */ public function isEncryptedPath($path) { // Disable encryption proxy so data retrieved is in its // original form $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $data = ''; // we only need 24 byte from the last chunk if ($this->view->file_exists($path)) { $handle = $this->view->fopen($path, 'r'); if (is_resource($handle)) { // suppress fseek warining, we handle the case that fseek doesn't // work in the else branch if (@fseek($handle, -24, SEEK_END) === 0) { $data = fgets($handle); } else { // if fseek failed on the storage we create a local copy from the file // and read this one fclose($handle); $localFile = $this->view->getLocalFile($path); $handle = fopen($localFile, 'r'); if (is_resource($handle) && fseek($handle, -24, SEEK_END) === 0) { $data = fgets($handle); } } fclose($handle); } } // re-enable proxy \OC_FileProxy::$enabled = $proxyStatus; return Crypt::isCatfileContent($data); } /** * get the file size of the unencrypted file * @param string $path absolute path * @return bool */ public function getFileSize($path) { $result = 0; // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; // split the path parts $pathParts = explode('/', $path); if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path) && $this->isEncryptedPath($path) ) { $cipher = 'AES-128-CFB'; $realSize = 0; // get the size from filesystem $size = $this->view->filesize($path); // open stream $stream = $this->view->fopen($path, "r"); if (is_resource($stream)) { // if the file contains a encryption header we // we set the cipher // and we update the size if ($this->containHeader($path)) { $data = fread($stream,Crypt::BLOCKSIZE); $header = Crypt::parseHeader($data); $cipher = Crypt::getCipher($header); $size -= Crypt::BLOCKSIZE; } // fast path, else the calculation for $lastChunkNr is bogus if ($size === 0) { \OC_FileProxy::$enabled = $proxyStatus; return 0; } // calculate last chunk nr // next highest is end of chunks, one subtracted is last one // we have to read the last chunk, we can't just calculate it (because of padding etc) $lastChunkNr = ceil($size/Crypt::BLOCKSIZE)-1; // calculate last chunk position $lastChunkPos = ($lastChunkNr * Crypt::BLOCKSIZE); // get the content of the last chunk if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) { $realSize+=$lastChunkNr*6126; } $lastChunkContentEncrypted=''; $count=Crypt::BLOCKSIZE; while ($count>0) { $data=fread($stream,Crypt::BLOCKSIZE); $count=strlen($data); $lastChunkContentEncrypted.=$data; if(strlen($lastChunkContentEncrypted)>Crypt::BLOCKSIZE) { $realSize+=6126; $lastChunkContentEncrypted=substr($lastChunkContentEncrypted,Crypt::BLOCKSIZE); } } fclose($stream); $relPath = Helper::stripUserFilesPath($path); $shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $relPath); if($shareKey===false) { \OC_FileProxy::$enabled = $proxyStatus; return $result; } $session = new Session($this->view); $privateKey = $session->getPrivateKey(); $plainKeyfile = $this->decryptKeyfile($relPath, $privateKey); $plainKey = Crypt::multiKeyDecrypt($plainKeyfile, $shareKey, $privateKey); $lastChunkContent=Crypt::symmetricDecryptFileContent($lastChunkContentEncrypted, $plainKey, $cipher); // calc the real file size with the size of the last chunk $realSize += strlen($lastChunkContent); // store file size $result = $realSize; } } \OC_FileProxy::$enabled = $proxyStatus; return $result; } /** * check if encrypted file contain a encryption header * * @param string $path * @return boolean */ private function containHeader($path) { // Disable encryption proxy to read the raw data $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $isHeader = false; $handle = $this->view->fopen($path, 'r'); if (is_resource($handle)) { $firstBlock = fread($handle, Crypt::BLOCKSIZE); $isHeader = Crypt::isHeader($firstBlock); } \OC_FileProxy::$enabled = $proxyStatus; return $isHeader; } /** * fix the file size of the encrypted file * @param string $path absolute path * @return boolean true / false if file is encrypted */ public function fixFileSize($path) { $result = false; // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $realSize = $this->getFileSize($path); if ($realSize > 0) { $cached = $this->view->getFileInfo($path); $cached['encrypted'] = true; // set the size $cached['unencrypted_size'] = $realSize; // put file info $this->view->putFileInfo($path, $cached); $result = true; } \OC_FileProxy::$enabled = $proxyStatus; return $result; } /** * encrypt versions from given file * @param array $filelist list of encrypted files, relative to data/user/files * @return boolean */ private function encryptVersions($filelist) { $successful = true; if (\OCP\App::isEnabled('files_versions')) { foreach ($filelist as $filename) { $versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename); foreach ($versions as $version) { $path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version']; $encHandle = fopen('crypt://' . $path . '.part', 'wb'); if ($encHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } $plainHandle = $this->view->fopen($path, 'rb'); if ($plainHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } stream_copy_to_stream($plainHandle, $encHandle); fclose($encHandle); fclose($plainHandle); $this->view->rename($path . '.part', $path); } } } return $successful; } /** * decrypt versions from given file * @param string $filelist list of decrypted files, relative to data/user/files * @return boolean */ private function decryptVersions($filelist) { $successful = true; if (\OCP\App::isEnabled('files_versions')) { foreach ($filelist as $filename) { $versions = \OCA\Files_Versions\Storage::getVersions($this->userId, $filename); foreach ($versions as $version) { $path = '/' . $this->userId . '/files_versions/' . $version['path'] . '.v' . $version['version']; $encHandle = fopen('crypt://' . $path, 'rb'); if ($encHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } $plainHandle = $this->view->fopen($path . '.part', 'wb'); if ($plainHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $path . '.part", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } stream_copy_to_stream($encHandle, $plainHandle); fclose($encHandle); fclose($plainHandle); $this->view->rename($path . '.part', $path); } } } return $successful; } /** * Decrypt all files * @return bool */ public function decryptAll() { $found = $this->findEncFiles($this->userId . '/files'); $successful = true; if ($found) { $versionStatus = \OCP\App::isEnabled('files_versions'); \OC_App::disable('files_versions'); $decryptedFiles = array(); // Encrypt unencrypted files foreach ($found['encrypted'] as $encryptedFile) { //relative to data//file $relPath = Helper::stripUserFilesPath($encryptedFile['path']); //get file info $fileInfo = \OC\Files\Filesystem::getFileInfo($relPath); //relative to /data $rawPath = $encryptedFile['path']; //get timestamp $timestamp = $fileInfo['mtime']; //enable proxy to use OC\Files\View to access the original file \OC_FileProxy::$enabled = true; // Open enc file handle for binary reading $encHandle = $this->view->fopen($rawPath, 'rb'); // Disable proxy to prevent file being encrypted again \OC_FileProxy::$enabled = false; if ($encHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } // Open plain file handle for binary writing, with same filename as original plain file $plainHandle = $this->view->fopen($rawPath . '.part', 'wb'); if ($plainHandle === false) { \OCP\Util::writeLog('Encryption library', 'couldn\'t open "' . $rawPath . '.part", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } // Move plain file to a temporary location $size = stream_copy_to_stream($encHandle, $plainHandle); if ($size === 0) { \OCP\Util::writeLog('Encryption library', 'Zero bytes copied of "' . $rawPath . '", decryption failed!', \OCP\Util::FATAL); $successful = false; continue; } fclose($encHandle); fclose($plainHandle); $fakeRoot = $this->view->getRoot(); $this->view->chroot('/' . $this->userId . '/files'); $this->view->rename($relPath . '.part', $relPath); //set timestamp $this->view->touch($relPath, $timestamp); $this->view->chroot($fakeRoot); // Add the file to the cache \OC\Files\Filesystem::putFileInfo($relPath, array( 'encrypted' => false, 'size' => $size, 'unencrypted_size' => 0, 'etag' => $fileInfo['etag'] )); $decryptedFiles[] = $relPath; } if ($versionStatus) { \OC_App::enable('files_versions'); } if (!$this->decryptVersions($decryptedFiles)) { $successful = false; } // if there are broken encrypted files than the complete decryption // was not successful if (!empty($found['broken'])) { $successful = false; } if ($successful) { $this->backupAllKeys('decryptAll'); $this->view->deleteAll($this->keysPath); } \OC_FileProxy::$enabled = true; } return $successful; } /** * Encrypt all files in a directory * @param string $dirPath the directory whose files will be encrypted * @return bool * @note Encryption is recursive */ public function encryptAll($dirPath) { $result = true; $found = $this->findEncFiles($dirPath); // Disable proxy to prevent file being encrypted twice \OC_FileProxy::$enabled = false; $versionStatus = \OCP\App::isEnabled('files_versions'); \OC_App::disable('files_versions'); $encryptedFiles = array(); // Encrypt unencrypted files foreach ($found['plain'] as $plainFile) { //get file info $fileInfo = \OC\Files\Filesystem::getFileInfo($plainFile['path']); //relative to data//file $relPath = $plainFile['path']; //relative to /data $rawPath = '/' . $this->userId . '/files/' . $plainFile['path']; // keep timestamp $timestamp = $fileInfo['mtime']; // Open plain file handle for binary reading $plainHandle = $this->view->fopen($rawPath, 'rb'); // Open enc file handle for binary writing, with same filename as original plain file $encHandle = fopen('crypt://' . $rawPath . '.part', 'wb'); if (is_resource($encHandle) && is_resource($plainHandle)) { // Move plain file to a temporary location $size = stream_copy_to_stream($plainHandle, $encHandle); fclose($encHandle); fclose($plainHandle); $fakeRoot = $this->view->getRoot(); $this->view->chroot('/' . $this->userId . '/files'); $this->view->rename($relPath . '.part', $relPath); // set timestamp $this->view->touch($relPath, $timestamp); $encSize = $this->view->filesize($relPath); $this->view->chroot($fakeRoot); // Add the file to the cache \OC\Files\Filesystem::putFileInfo($relPath, array( 'encrypted' => true, 'size' => $encSize, 'unencrypted_size' => $size, 'etag' => $fileInfo['etag'] )); $encryptedFiles[] = $relPath; } else { \OCP\Util::writeLog('files_encryption', 'initial encryption: could not encrypt ' . $rawPath, \OCP\Util::FATAL); $result = false; } } \OC_FileProxy::$enabled = true; if ($versionStatus) { \OC_App::enable('files_versions'); } $result = $result && $this->encryptVersions($encryptedFiles); return $result; } /** * Return important encryption related paths * @param string $pathName Name of the directory to return the path of * @return string path */ public function getPath($pathName) { switch ($pathName) { case 'publicKeyDir': return $this->publicKeyDir; break; case 'encryptionDir': return $this->encryptionDir; break; case 'keysPath': return $this->keysPath; break; case 'publicKeyPath': return $this->publicKeyPath; break; case 'privateKeyPath': return $this->privateKeyPath; break; } return false; } /** * Returns whether the given user is ready for encryption. * Also returns true if the given user is the public user * or the recovery key user. * * @param string $user user to check * * @return boolean true if the user is ready, false otherwise */ private function isUserReady($user) { if ($user === $this->publicShareKeyId || $user === $this->recoveryKeyId ) { return true; } $util = new Util($this->view, $user); return $util->ready(); } /** * Filter an array of UIDs to return only ones ready for sharing * @param array $unfilteredUsers users to be checked for sharing readiness * @return array as multi-dimensional array. keys: ready, unready */ public function filterShareReadyUsers($unfilteredUsers) { // This array will collect the filtered IDs $readyIds = $unreadyIds = array(); // Loop through users and create array of UIDs that need new keyfiles foreach ($unfilteredUsers as $user) { // Check that the user is encryption capable, or is the // public system user (for public shares) if ($this->isUserReady($user)) { // Construct array of ready UIDs for Keymanager{} $readyIds[] = $user; } else { // Construct array of unready UIDs for Keymanager{} $unreadyIds[] = $user; // Log warning; we can't do necessary setup here // because we don't have the user passphrase \OCP\Util::writeLog('Encryption library', '"' . $user . '" is not setup for encryption', \OCP\Util::WARN); } } return array( 'ready' => $readyIds, 'unready' => $unreadyIds ); } /** * Decrypt a keyfile * @param string $filePath * @param string $privateKey * @return false|string */ private function decryptKeyfile($filePath, $privateKey) { // Get the encrypted keyfile $encKeyfile = Keymanager::getFileKey($this->view, $this, $filePath); // The file has a shareKey and must use it for decryption $shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $filePath); $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); return $plainKeyfile; } /** * Encrypt keyfile to multiple users * @param Session $session * @param array $users list of users which should be able to access the file * @param string $filePath path of the file to be shared * @return bool */ public function setSharedFileKeyfiles(Session $session, array $users, $filePath) { // Make sure users are capable of sharing $filteredUids = $this->filterShareReadyUsers($users); // If we're attempting to share to unready users if (!empty($filteredUids['unready'])) { \OCP\Util::writeLog('Encryption library', 'Sharing to these user(s) failed as they are unready for encryption:"' . print_r($filteredUids['unready'], 1), \OCP\Util::WARN); return false; } // Get public keys for each user, ready for generating sharekeys $userPubKeys = Keymanager::getPublicKeys($this->view, $filteredUids['ready']); // Note proxy status then disable it $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; // Get the current users's private key for decrypting existing keyfile $privateKey = $session->getPrivateKey(); try { // Decrypt keyfile $plainKeyfile = $this->decryptKeyfile($filePath, $privateKey); // Re-enc keyfile to (additional) sharekeys $multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys); } catch (Exception\EncryptionException $e) { $msg = 'set shareFileKeyFailed (code: ' . $e->getCode() . '): ' . $e->getMessage(); \OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL); return false; } catch (\Exception $e) { $msg = 'set shareFileKeyFailed (unknown error): ' . $e->getMessage(); \OCP\Util::writeLog('files_encryption', $msg, \OCP\Util::FATAL); return false; } // Save the recrypted key to it's owner's keyfiles directory // Save new sharekeys to all necessary user directory if ( !Keymanager::setFileKey($this->view, $this, $filePath, $multiEncKey['data']) || !Keymanager::setShareKeys($this->view, $this, $filePath, $multiEncKey['keys']) ) { \OCP\Util::writeLog('Encryption library', 'Keyfiles could not be saved for users sharing ' . $filePath, \OCP\Util::ERROR); return false; } // Return proxy to original status \OC_FileProxy::$enabled = $proxyStatus; return true; } /** * Find, sanitise and format users sharing a file * @note This wraps other methods into a portable bundle * @param boolean $sharingEnabled * @param string $filePath path relativ to current users files folder */ public function getSharingUsersArray($sharingEnabled, $filePath) { $appConfig = \OC::$server->getAppConfig(); // Check if key recovery is enabled if ( $appConfig->getValue('files_encryption', 'recoveryAdminEnabled') && $this->recoveryEnabledForUser() ) { $recoveryEnabled = true; } else { $recoveryEnabled = false; } // Make sure that a share key is generated for the owner too list($owner, $ownerPath) = $this->getUidAndFilename($filePath); $ownerPath = Helper::stripPartialFileExtension($ownerPath); // always add owner to the list of users with access to the file $userIds = array($owner); if ($sharingEnabled) { // Find out who, if anyone, is sharing the file $result = \OCP\Share::getUsersSharingFile($ownerPath, $owner); $userIds = \array_merge($userIds, $result['users']); if ($result['public']) { $userIds[] = $this->publicShareKeyId; } } // If recovery is enabled, add the // Admin UID to list of users to share to if ($recoveryEnabled) { // Find recoveryAdmin user ID $recoveryKeyId = $appConfig->getValue('files_encryption', 'recoveryKeyId'); // Add recoveryAdmin to list of users sharing $userIds[] = $recoveryKeyId; } // check if it is a group mount if (\OCP\App::isEnabled("files_external")) { $mounts = \OC_Mount_Config::getSystemMountPoints(); foreach ($mounts as $mount) { if ($mount['mountpoint'] == substr($ownerPath, 1, strlen($mount['mountpoint']))) { $userIds = array_merge($userIds, $this->getUserWithAccessToMountPoint($mount['applicable']['users'], $mount['applicable']['groups'])); } } } // Remove duplicate UIDs $uniqueUserIds = array_unique($userIds); return $uniqueUserIds; } private function getUserWithAccessToMountPoint($users, $groups) { $result = array(); if (in_array('all', $users)) { $result = \OCP\User::getUsers(); } else { $result = array_merge($result, $users); foreach ($groups as $group) { $result = array_merge($result, \OC_Group::usersInGroup($group)); } } return $result; } /** * set migration status * @param int $status * @param int $preCondition only update migration status if the previous value equals $preCondition * @return boolean */ private function setMigrationStatus($status, $preCondition = null) { // convert to string if preCondition is set $preCondition = ($preCondition === null) ? null : (string)$preCondition; try { \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)$status, $preCondition); return true; } catch(\OCP\PreConditionNotMetException $e) { return false; } } /** * start migration mode to initially encrypt users data * @return boolean */ public function beginMigration() { $result = $this->setMigrationStatus(self::MIGRATION_IN_PROGRESS, self::MIGRATION_OPEN); if ($result) { \OCP\Util::writeLog('Encryption library', "Start migration to encryption mode for " . $this->userId, \OCP\Util::INFO); } else { \OCP\Util::writeLog('Encryption library', "Could not activate migration mode for " . $this->userId . ". Probably another process already started the initial encryption", \OCP\Util::WARN); } return $result; } public function resetMigrationStatus() { return $this->setMigrationStatus(self::MIGRATION_OPEN); } /** * close migration mode after users data has been encrypted successfully * @return boolean */ public function finishMigration() { $result = $this->setMigrationStatus(self::MIGRATION_COMPLETED); if ($result) { \OCP\Util::writeLog('Encryption library', "Finish migration successfully for " . $this->userId, \OCP\Util::INFO); } else { \OCP\Util::writeLog('Encryption library', "Could not deactivate migration mode for " . $this->userId, \OCP\Util::WARN); } return $result; } /** * check if files are already migrated to the encryption system * @return int|false migration status, false = in case of no record * @note If records are not being returned, check for a hidden space * at the start of the uid in db */ public function getMigrationStatus() { $migrationStatus = false; if (\OCP\User::userExists($this->userId)) { $migrationStatus = \OC::$server->getConfig()->getUserValue($this->userId, 'files_encryption', 'migration_status', null); if ($migrationStatus === null) { \OC::$server->getConfig()->setUserValue($this->userId, 'files_encryption', 'migration_status', (string)self::MIGRATION_OPEN); $migrationStatus = self::MIGRATION_OPEN; } } return (int)$migrationStatus; } /** * get uid of the owners of the file and the path to the file * @param string $path Path of the file to check * @throws \Exception * @note $shareFilePath must be relative to data/UID/files. Files * relative to /Shared are also acceptable * @return array */ public function getUidAndFilename($path) { $pathinfo = pathinfo($path); $partfile = false; $parentFolder = false; if (array_key_exists('extension', $pathinfo) && $pathinfo['extension'] === 'part') { // if the real file exists we check this file $filePath = $this->userFilesDir . '/' .$pathinfo['dirname'] . '/' . $pathinfo['filename']; if ($this->view->file_exists($filePath)) { $pathToCheck = $pathinfo['dirname'] . '/' . $pathinfo['filename']; } else { // otherwise we look for the parent $pathToCheck = $pathinfo['dirname']; $parentFolder = true; } $partfile = true; } else { $pathToCheck = $path; } $view = new \OC\Files\View($this->userFilesDir); $fileOwnerUid = $view->getOwner($pathToCheck); // handle public access if ($this->isPublic) { return array($this->userId, $path); } else { // Check that UID is valid if (!\OCP\User::userExists($fileOwnerUid)) { throw new \Exception( 'Could not find owner (UID = "' . var_export($fileOwnerUid, 1) . '") of file "' . $path . '"'); } // NOTE: Bah, this dependency should be elsewhere \OC\Files\Filesystem::initMountPoints($fileOwnerUid); // If the file owner is the currently logged in user if ($fileOwnerUid === $this->userId) { // Assume the path supplied is correct $filename = $path; } else { $info = $view->getFileInfo($pathToCheck); $ownerView = new \OC\Files\View('/' . $fileOwnerUid . '/files'); // Fetch real file path from DB $filename = $ownerView->getPath($info['fileid']); if ($parentFolder) { $filename = $filename . '/'. $pathinfo['filename']; } if ($partfile) { $filename = $filename . '.' . $pathinfo['extension']; } } return array( $fileOwnerUid, \OC\Files\Filesystem::normalizePath($filename) ); } } /** * go recursively through a dir and collect all files and sub files. * @param string $dir relative to the users files folder * @return array with list of files relative to the users files folder */ public function getAllFiles($dir, $mountPoint = '') { $result = array(); $dirList = array($dir); while ($dirList) { $dir = array_pop($dirList); $content = $this->view->getDirectoryContent(\OC\Files\Filesystem::normalizePath( $this->userFilesDir . '/' . $dir)); foreach ($content as $c) { // getDirectoryContent() returns the paths relative to the mount points, so we need // to re-construct the complete path $path = ($mountPoint !== '') ? $mountPoint . '/' . $c['path'] : $c['path']; $path = \OC\Files\Filesystem::normalizePath($path); if ($c['type'] === 'dir') { $dirList[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files")); } else { $result[] = substr($path, strlen('/' . \OCP\User::getUser() . "/files")); } } } return $result; } /** * get owner of the shared files. * @param int $id ID of a share * @return string owner */ public function getOwnerFromSharedFile($id) { $query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1); $result = $query->execute(array($id)); $source = null; if (\OCP\DB::isError($result)) { \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); } else { $source = $result->fetchRow(); } $fileOwner = false; if ($source && isset($source['parent'])) { $parent = $source['parent']; while (isset($parent)) { $query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1); $result = $query->execute(array($parent)); $item = null; if (\OCP\DB::isError($result)) { \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); } else { $item = $result->fetchRow(); } if ($item && isset($item['parent'])) { $parent = $item['parent']; } else { $fileOwner = $item['uid_owner']; break; } } } else { $fileOwner = $source['uid_owner']; } return $fileOwner; } /** * @return string */ public function getUserId() { return $this->userId; } /** * @return string */ public function getKeyId() { return $this->keyId; } /** * @return string */ public function getUserFilesDir() { return $this->userFilesDir; } /** * @param string $password * @return bool */ public function checkRecoveryPassword($password) { $result = false; $recoveryKey = Keymanager::getPrivateSystemKey($this->recoveryKeyId); $decryptedRecoveryKey = Crypt::decryptPrivateKey($recoveryKey, $password); if ($decryptedRecoveryKey) { $result = true; } return $result; } /** * @return string */ public function getRecoveryKeyId() { return $this->recoveryKeyId; } /** * add recovery key to all encrypted files */ public function addRecoveryKeys($path = '/') { $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); foreach ($dirContent as $item) { // get relative path from files_encryption/keyfiles/ $filePath = substr($item['path'], strlen('files_encryption/keys')); if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { $this->addRecoveryKeys($filePath . '/'); } else { $session = new Session(new \OC\Files\View('/')); $sharingEnabled = \OCP\Share::isEnabled(); $usersSharing = $this->getSharingUsersArray($sharingEnabled, $filePath); $this->setSharedFileKeyfiles($session, $usersSharing, $filePath); } } } /** * remove recovery key to all encrypted files */ public function removeRecoveryKeys($path = '/') { $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); foreach ($dirContent as $item) { // get relative path from files_encryption/keyfiles $filePath = substr($item['path'], strlen('files_encryption/keys')); if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { $this->removeRecoveryKeys($filePath . '/'); } else { $this->view->unlink($this->keysPath . '/' . $filePath . '/' . $this->recoveryKeyId . '.shareKey'); } } } /** * decrypt given file with recovery key and encrypt it again to the owner and his new key * @param string $file * @param string $privateKey recovery key to decrypt the file */ private function recoverFile($file, $privateKey) { $sharingEnabled = \OCP\Share::isEnabled(); // Find out who, if anyone, is sharing the file if ($sharingEnabled) { $result = \OCP\Share::getUsersSharingFile($file, $this->userId, true); $userIds = $result['users']; $userIds[] = $this->recoveryKeyId; if ($result['public']) { $userIds[] = $this->publicShareKeyId; } } else { $userIds = array( $this->userId, $this->recoveryKeyId ); } $filteredUids = $this->filterShareReadyUsers($userIds); //decrypt file key $encKeyfile = Keymanager::getFileKey($this->view, $this, $file); $shareKey = Keymanager::getShareKey($this->view, $this->recoveryKeyId, $this, $file); $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); // encrypt file key again to all users, this time with the new public key for the recovered use $userPubKeys = Keymanager::getPublicKeys($this->view, $filteredUids['ready']); $multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys); Keymanager::setFileKey($this->view, $this, $file, $multiEncKey['data']); Keymanager::setShareKeys($this->view, $this, $file, $multiEncKey['keys']); } /** * collect all files and recover them one by one * @param string $path to look for files keys * @param string $privateKey private recovery key which is used to decrypt the files */ private function recoverAllFiles($path, $privateKey) { $dirContent = $this->view->getDirectoryContent($this->keysPath . '/' . $path); foreach ($dirContent as $item) { // get relative path from files_encryption/keyfiles $filePath = substr($item['path'], strlen('files_encryption/keys')); if ($this->view->is_dir($this->userFilesDir . '/' . $filePath)) { $this->recoverAllFiles($filePath . '/', $privateKey); } else { $this->recoverFile($filePath, $privateKey); } } } /** * recover users files in case of password lost * @param string $recoveryPassword */ public function recoverUsersFiles($recoveryPassword) { $encryptedKey = Keymanager::getPrivateSystemKey( $this->recoveryKeyId); $privateKey = Crypt::decryptPrivateKey($encryptedKey, $recoveryPassword); $this->recoverAllFiles('/', $privateKey); } /** * create a backup of all keys from the user * * @param string $purpose (optional) define the purpose of the backup, will be part of the backup folder */ public function backupAllKeys($purpose = '') { $this->userId; $backupDir = $this->encryptionDir . '/backup.'; $backupDir .= ($purpose === '') ? date("Y-m-d_H-i-s") . '/' : $purpose . '.' . date("Y-m-d_H-i-s") . '/'; $this->view->mkdir($backupDir); $this->view->copy($this->keysPath, $backupDir . 'keys/'); $this->view->copy($this->privateKeyPath, $backupDir . $this->userId . '.privateKey'); $this->view->copy($this->publicKeyPath, $backupDir . $this->userId . '.publicKey'); } /** * check if the file is stored on a system wide mount point * @param string $path relative to /data/user with leading '/' * @return boolean */ public function isSystemWideMountPoint($path) { $normalizedPath = ltrim($path, '/'); if (\OCP\App::isEnabled("files_external")) { $mounts = \OC_Mount_Config::getSystemMountPoints(); foreach ($mounts as $mount) { if ($mount['mountpoint'] == substr($normalizedPath, 0, strlen($mount['mountpoint']))) { if ($this->isMountPointApplicableToUser($mount)) { return true; } } } } return false; } /** * check if mount point is applicable to user * * @param array $mount contains $mount['applicable']['users'], $mount['applicable']['groups'] * @return boolean */ protected function isMountPointApplicableToUser($mount) { $uid = \OCP\User::getUser(); $acceptedUids = array('all', $uid); // check if mount point is applicable for the user $intersection = array_intersect($acceptedUids, $mount['applicable']['users']); if (!empty($intersection)) { return true; } // check if mount point is applicable for group where the user is a member foreach ($mount['applicable']['groups'] as $gid) { if (\OC_Group::inGroup($uid, $gid)) { return true; } } return false; } /** * decrypt private key and add it to the current session * @param array $params with 'uid' and 'password' * @return mixed session or false */ public function initEncryption($params) { $session = new Session($this->view); // we tried to initialize the encryption app for this session $session->setInitialized(Session::INIT_EXECUTED); $encryptedKey = Keymanager::getPrivateKey($this->view, $params['uid']); $privateKey = false; if ($encryptedKey) { $privateKey = Crypt::decryptPrivateKey($encryptedKey, $params['password']); } if ($privateKey === false) { \OCP\Util::writeLog('Encryption library', 'Private key for user "' . $params['uid'] . '" is not valid! Maybe the user password was changed from outside if so please change it back to gain access', \OCP\Util::ERROR); return false; } $session->setPrivateKey($privateKey); $session->setInitialized(Session::INIT_SUCCESSFUL); return $session; } /* * remove encryption related keys from the session */ public function closeEncryptionSession() { $session = new Session($this->view); $session->closeSession(); } }