diff options
Diffstat (limited to 'apps/files_encryption/lib/util.php')
-rw-r--r-- | apps/files_encryption/lib/util.php | 1639 |
1 files changed, 1355 insertions, 284 deletions
diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php index 52bc74db27a..04bd4dc8aca 100644 --- a/apps/files_encryption/lib/util.php +++ b/apps/files_encryption/lib/util.php @@ -3,8 +3,8 @@ * ownCloud * * @author Sam Tuke, Frank Karlitschek - * @copyright 2012 Sam Tuke samtuke@owncloud.com, - * Frank Karlitschek frank@owncloud.org + * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, + * Frank Karlitschek <frank@owncloud.org> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -21,78 +21,82 @@ * */ -// Todo: +# Bugs +# ---- +# Sharing a file to a user without encryption set up will not provide them with access but won't notify the sharer +# Sharing all files to admin for recovery purposes still in progress +# Possibly public links are broken (not tested since last merge of master) + + +# Missing features +# ---------------- +# Make sure user knows if large files weren't encrypted + + +# Test +# ---- +# Test that writing files works when recovery is enabled, and sharing API is disabled +# Test trashbin support + + +// Old Todo: // - Crypt/decrypt button in the userinterface // - Setting if crypto should be on by default // - Add a setting "Don´t encrypt files larger than xx because of performance // reasons" -// - Transparent decrypt/encrypt in filesystem.php. Autodetect if a file is -// encrypted (.encrypted extension) -// - Don't use a password directly as encryption key. but a key which is -// stored on the server and encrypted with the user password. -> password -// change faster -// - IMPORTANT! Check if the block lenght of the encrypted data stays the same namespace OCA\Encryption; /** * @brief Class for utilities relating to encrypted file storage system - * @param OC_FilesystemView $view expected to have OC '/' as root path + * @param \OC_FilesystemView $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 { - - + // Web UI: - + //// DONE: files created via web ui are encrypted //// DONE: file created & encrypted via web ui are readable in web ui //// DONE: file created & encrypted via web ui are readable via webdav - - + + // WebDAV: - + //// DONE: new data filled files added via webdav get encrypted //// DONE: new data filled files added via webdav are readable via webdav //// DONE: reading unencrypted files when encryption is enabled works via //// webdav //// DONE: files created & encrypted via web ui are readable via webdav - - + + // Legacy support: - + //// DONE: add method to check if file is encrypted using new system //// DONE: add method to check if file is encrypted using old system //// DONE: add method to fetch legacy key //// DONE: add method to decrypt legacy encrypted data - - + + // Admin UI: - + //// DONE: changing user password also changes encryption passphrase - + //// TODO: add support for optional recovery in case of lost passphrase / keys //// TODO: add admin optional required long passphrase for users - //// TODO: add UI buttons for encrypt / decrypt everything //// TODO: implement flag system to allow user to specify encryption by folder, subfolder, etc. - - - // Sharing: - - //// TODO: add support for encrypting to multiple public keys - //// TODO: add support for decrypting to multiple private keys - - + + // Integration testing: - + //// TODO: test new encryption with versioning - //// TODO: test new encryption with sharing + //// DONE: test new encryption with sharing //// TODO: test new encryption with proxies - - + + private $view; // OC_FilesystemView object for filesystem operations private $userId; // ID of the currently logged-in user private $pwd; // User Password @@ -103,166 +107,322 @@ class Util { private $shareKeysPath; // Dir containing env keys for shared files private $publicKeyPath; // Path to user's public key private $privateKeyPath; // Path to user's private key + private $publicShareKeyId; + private $recoveryKeyId; + private $isPublic; + + /** + * @param \OC_FilesystemView $view + * @param $userId + * @param bool $client + */ + public function __construct(\OC_FilesystemView $view, $userId, $client = false) { - public function __construct( \OC_FilesystemView $view, $userId, $client = false ) { - $this->view = $view; $this->userId = $userId; $this->client = $client; - $this->userDir = '/' . $this->userId; - $this->userFilesDir = '/' . $this->userId . '/' . 'files'; - $this->publicKeyDir = '/' . 'public-keys'; - $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; - $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; - $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; - $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key - $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key - + $this->isPublic = false; + + $this->publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId'); + $this->recoveryKeyId = \OC_Appconfig::getValue('files_encryption', 'recoveryKeyId'); + + // if we are anonymous/public + if (\OCA\Encryption\Helper::isPublicAccess()) { + $this->userId = $this->publicShareKeyId; + + // only handle for files_sharing app + if (isset($GLOBALS['app']) && $GLOBALS['app'] === 'files_sharing') { + $this->userDir = '/' . $GLOBALS['fileOwner']; + $this->fileFolderName = 'files'; + $this->userFilesDir = '/' . $GLOBALS['fileOwner'] . '/' + . $this->fileFolderName; // TODO: Does this need to be user configurable? + $this->publicKeyDir = '/' . 'public-keys'; + $this->encryptionDir = '/' . $GLOBALS['fileOwner'] . '/' . 'files_encryption'; + $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; + $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; + $this->publicKeyPath = + $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key + $this->privateKeyPath = + '/owncloud_private_key/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key + $this->isPublic = true; + } + + } else { + $this->userDir = '/' . $this->userId; + $this->fileFolderName = 'files'; + $this->userFilesDir = + '/' . $this->userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? + $this->publicKeyDir = '/' . 'public-keys'; + $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; + $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; + $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; + $this->publicKeyPath = + $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key + $this->privateKeyPath = + $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key + } } - + + /** + * @return bool + */ public function ready() { - - if( - !$this->view->file_exists( $this->encryptionDir ) - or !$this->view->file_exists( $this->keyfilesPath ) - or !$this->view->file_exists( $this->shareKeysPath ) - or !$this->view->file_exists( $this->publicKeyPath ) - or !$this->view->file_exists( $this->privateKeyPath ) + + if ( + !$this->view->file_exists($this->encryptionDir) + or !$this->view->file_exists($this->keyfilesPath) + or !$this->view->file_exists($this->shareKeysPath) + or !$this->view->file_exists($this->publicKeyPath) + or !$this->view->file_exists($this->privateKeyPath) ) { - + return false; - + } else { - + return true; - + } - + } - - /** - * @brief Sets up user folders and keys for serverside encryption - * @param $passphrase passphrase to encrypt server-stored private key with - */ - public function setupServerSide( $passphrase = null ) { - - // Create user dir - if( !$this->view->file_exists( $this->userDir ) ) { - - $this->view->mkdir( $this->userDir ); - - } - - // Create user files dir - if( !$this->view->file_exists( $this->userFilesDir ) ) { - - $this->view->mkdir( $this->userFilesDir ); - - } - - // Create shared public key directory - if( !$this->view->file_exists( $this->publicKeyDir ) ) { - - $this->view->mkdir( $this->publicKeyDir ); - - } - - // Create encryption app directory - if( !$this->view->file_exists( $this->encryptionDir ) ) { - - $this->view->mkdir( $this->encryptionDir ); - - } - - // Create mirrored keyfile directory - if( !$this->view->file_exists( $this->keyfilesPath ) ) { - - $this->view->mkdir( $this->keyfilesPath ); - - } - - // Create mirrored share env keys directory - if( !$this->view->file_exists( $this->shareKeysPath ) ) { - - $this->view->mkdir( $this->shareKeysPath ); - - } - + + /** + * @brief 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->userFilesDir, + $this->publicKeyDir, + $this->encryptionDir, + $this->keyfilesPath, + $this->shareKeysPath + ); + + // Check / create all necessary dirs + foreach ($setUpDirs as $dirPath) { + + if (!$this->view->file_exists($dirPath)) { + + $this->view->mkdir($dirPath); + + } + + } + // Create user keypair - if ( - ! $this->view->file_exists( $this->publicKeyPath ) - or ! $this->view->file_exists( $this->privateKeyPath ) + // we should never override a keyfile + if ( + !$this->view->file_exists($this->publicKeyPath) + && !$this->view->file_exists($this->privateKeyPath) ) { - + // Generate keypair $keypair = Crypt::createKeypair(); - + \OC_FileProxy::$enabled = false; - + // Save public key - $this->view->file_put_contents( $this->publicKeyPath, $keypair['publicKey'] ); - + $this->view->file_put_contents($this->publicKeyPath, $keypair['publicKey']); + // Encrypt private key with user pwd as passphrase - $encryptedPrivateKey = Crypt::symmetricEncryptFileContent( $keypair['privateKey'], $passphrase ); - + $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase); + // Save private key - $this->view->file_put_contents( $this->privateKeyPath, $encryptedPrivateKey ); - + $this->view->file_put_contents($this->privateKeyPath, $encryptedPrivateKey); + \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->userId . '"', \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->userId . '"', \OCP\Util::FATAL); + return false; + } + } } - + + // If there's no record for this user's encryption preferences + if (false === $this->recoveryEnabledForUser()) { + + // create database configuration + $sql = 'INSERT INTO `*PREFIX*encryption` (`uid`,`mode`,`recovery_enabled`) VALUES (?,?,?)'; + $args = array( + $this->userId, + 'server-side', + 0 + ); + $query = \OCP\DB::prepare($sql); + $query->execute($args); + + } + return true; - + } - + + /** + * @return string + */ + public function getPublicShareKeyId() { + return $this->publicShareKeyId; + } + + /** + * @brief 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() { + + $sql = 'SELECT `recovery_enabled` FROM `*PREFIX*encryption` WHERE uid = ?'; + + $args = array($this->userId); + + $query = \OCP\DB::prepare($sql); + + $result = $query->execute($args); + + $recoveryEnabled = array(); + + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $row = $result->fetchRow(); + if (isset($row['recovery_enabled'])) { + $recoveryEnabled[] = $row['recovery_enabled']; + } + } + } + + // If no record is found + if (empty($recoveryEnabled)) { + + return false; + + // If a record is found + } else { + + return $recoveryEnabled[0]; + + } + + } + + /** + * @brief Enable / disable pwd recovery for a given user + * @param bool $enabled Whether to enable or disable recovery + * @return bool + */ + public function setRecoveryForUser($enabled) { + + $recoveryStatus = $this->recoveryEnabledForUser(); + + // If a record for this user already exists, update it + if (false === $recoveryStatus) { + + $sql = 'INSERT INTO `*PREFIX*encryption` (`uid`,`mode`,`recovery_enabled`) VALUES (?,?,?)'; + + $args = array( + $this->userId, + 'server-side', + $enabled + ); + + // Create a new record instead + } else { + + $sql = 'UPDATE `*PREFIX*encryption` SET recovery_enabled = ? WHERE uid = ?'; + + $args = array( + $enabled, + $this->userId + ); + + } + + $query = \OCP\DB::prepare($sql); + + if ($query->execute($args)) { + + return true; + + } else { + + return false; + + } + + } + /** * @brief 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 mixed false if 0 found, array on success. Keys: name, path - * @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 findFiles( $directory ) { - + public function findEncFiles($directory, &$found = false) { + // Disable proxy - we don't want files to be decrypted before // we handle them \OC_FileProxy::$enabled = false; - - $found = array( 'plain' => array(), 'encrypted' => array(), 'legacy' => array() ); - - if ( - $this->view->is_dir( $directory ) - && $handle = $this->view->opendir( $directory ) + + if ($found === false) { + $found = array( + 'plain' => array(), + 'encrypted' => array(), + 'legacy' => array() + ); + } + + if ( + $this->view->is_dir($directory) + && $handle = $this->view->opendir($directory) ) { - - while ( false !== ( $file = readdir( $handle ) ) ) { - + + while (false !== ($file = readdir($handle))) { + if ( - $file != "." - && $file != ".." + $file !== "." + && $file !== ".." ) { - - $filePath = $directory . '/' . $this->view->getRelativePath( '/' . $file ); - $relPath = $this->stripUserFilesPath( $filePath ); - + + $filePath = $directory . '/' . $this->view->getRelativePath('/' . $file); + $relPath = \OCA\Encryption\Helper::stripUserFilesPath($filePath); + // If the path is a directory, search // its contents - if ( $this->view->is_dir( $filePath ) ) { - - $this->findFiles( $filePath ); - - // If the path is a file, determine - // its encryption status - } elseif ( $this->view->is_file( $filePath ) ) { - + 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; - - $data = $this->view->file_get_contents( $filePath ); - + + $data = $this->view->file_get_contents($filePath); + // If the file is encrypted // NOTE: If the userId is // empty or not set, file will @@ -270,207 +430,1118 @@ class Util { // NOTE: This is inefficient; // scanning every file like this // will eat server resources :( - if ( - Keymanager::getFileKey( $this->view, $this->userId, $file ) - && Crypt::isCatfile( $data ) + if ( + Keymanager::getFileKey($this->view, $this->userId, $relPath) + && Crypt::isCatfileContent($data) ) { - - $found['encrypted'][] = array( 'name' => $file, 'path' => $filePath ); - - // If the file uses old - // encryption system - } elseif ( Crypt::isLegacyEncryptedContent( $this->view->file_get_contents( $filePath ), $relPath ) ) { - - $found['legacy'][] = array( 'name' => $file, 'path' => $filePath ); - - // If the file is not encrypted + + $found['encrypted'][] = array( + 'name' => $file, + 'path' => $filePath + ); + + // If the file uses old + // encryption system + } elseif ( Crypt::isLegacyEncryptedContent( $data, $relPath ) ) { + + $found['legacy'][] = array( + 'name' => $file, + 'path' => $filePath + ); + + // If the file is not encrypted } else { - - $found['plain'][] = array( 'name' => $file, 'path' => $filePath ); - + + $found['plain'][] = array( + 'name' => $file, + 'path' => $relPath + ); + } - + } - + } - + } - + \OC_FileProxy::$enabled = true; - - if ( empty( $found ) ) { - + + if (empty($found)) { + return false; - + } else { - + return $found; - + } - + } - + \OC_FileProxy::$enabled = true; - + return false; } - - /** - * @brief Check if a given path identifies an encrypted file - * @return true / false - */ - public function isEncryptedPath( $path ) { - - // Disable encryption proxy so data retreived is in its - // original form + + /** + * @brief Fetch the last lines of a file efficiently + * @note Safe to use on large files; does not read entire file to memory + * @note Derivative of http://tekkie.flashbit.net/php/tail-functionality-in-php + */ + public function tail($filename, $numLines) { + \OC_FileProxy::$enabled = false; - - $data = $this->view->file_get_contents( $path ); - + + $text = ''; + $pos = -1; + $handle = $this->view->fopen($filename, 'r'); + + while ($numLines > 0) { + + --$pos; + + if (fseek($handle, $pos, SEEK_END) !== 0) { + + rewind($handle); + $numLines = 0; + + } elseif (fgetc($handle) === "\n") { + + --$numLines; + + } + + $block_size = (-$pos) % 8192; + if ($block_size === 0 || $numLines === 0) { + + $text = fread($handle, ($block_size === 0 ? 8192 : $block_size)) . $text; + + } + } + + fclose($handle); + \OC_FileProxy::$enabled = true; - - return Crypt::isCatfile( $data ); - + + return $text; } - + /** - * @brief Format a path to be relative to the /user/files/ directory + * @brief Check if a given path identifies an encrypted file + * @param string $path + * @return boolean */ - public function stripUserFilesPath( $path ) { - - $trimmed = ltrim( $path, '/' ); - $split = explode( '/', $trimmed ); - $sliced = array_slice( $split, 2 ); - $relPath = implode( '/', $sliced ); - - return $relPath; - + public function isEncryptedPath($path) { + + // Disable encryption proxy so data retrieved is in its + // original form + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // we only need 24 byte from the last chunk + $data = ''; + $handle = $this->view->fopen($path, 'r'); + if (is_resource($handle) && !fseek($handle, -24, SEEK_END)) { + $data = fgets($handle); + } + + // re-enable proxy + \OC_FileProxy::$enabled = $proxyStatus; + + return Crypt::isCatfileContent($data); + + } + + /** + * @brief 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); + + // get relative path + $relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path); + + if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path) && $this->isEncryptedPath($path)) { + + // get the size from filesystem + $fullPath = $this->view->getLocalFile($path); + $size = filesize($fullPath); + + // calculate last chunk nr + $lastChunkNr = floor($size / 8192); + + // open stream + $stream = fopen('crypt://' . $relativePath, "r"); + + if (is_resource($stream)) { + // calculate last chunk position + $lastChunckPos = ($lastChunkNr * 8192); + + // seek to end + fseek($stream, $lastChunckPos); + + // get the content of the last chunk + $lastChunkContent = fread($stream, 8192); + + // calc the real file size with the size of the last chunk + $realSize = (($lastChunkNr * 6126) + strlen($lastChunkContent)); + + // store file size + $result = $realSize; + } + } + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; } + + /** + * @brief 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; + } + /** + * @param $path + * @return bool + */ + public function isSharedPath($path) { + + $trimmed = ltrim($path, '/'); + $split = explode('/', $trimmed); + + if (isset($split[2]) && $split[2] === 'Shared') { + + return true; + + } else { + + return false; + + } + + } + + /** * @brief Encrypt all files in a directory - * @param string $publicKey the public key to encrypt files with * @param string $dirPath the directory whose files will be encrypted + * @param null $legacyPassphrase + * @param null $newPassphrase + * @return bool * @note Encryption is recursive */ - public function encryptAll( $publicKey, $dirPath, $legacyPassphrase = null, $newPassphrase = null ) { - - if ( $found = $this->findFiles( $dirPath ) ) { - + public function encryptAll($dirPath, $legacyPassphrase = null, $newPassphrase = null) { + + if ($found = $this->findEncFiles($dirPath)) { + // Disable proxy to prevent file being encrypted twice \OC_FileProxy::$enabled = false; - + // Encrypt unencrypted files - foreach ( $found['plain'] as $plainFile ) { - - // Fetch data from file - $plainData = $this->view->file_get_contents( $plainFile['path'] ); - - // Encrypt data, generate catfile - $encrypted = Crypt::keyEncryptKeyfile( $plainData, $publicKey ); - - $relPath = $this->stripUserFilesPath( $plainFile['path'] ); - - // Save keyfile - Keymanager::setFileKey( $this->view, $relPath, $this->userId, $encrypted['key'] ); - - // Overwrite the existing file with the encrypted one - $this->view->file_put_contents( $plainFile['path'], $encrypted['data'] ); - - $size = strlen( $encrypted['data'] ); - + foreach ($found['plain'] as $plainFile) { + + //relative to data/<user>/file + $relPath = $plainFile['path']; + + //relative to /data + $rawPath = '/'.$this->userId . '/files/' . $plainFile['path']; + + // 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://' . $relPath.'.tmp', 'wb' ); + + // Move plain file to a temporary location + $size = stream_copy_to_stream( $plainHandle, $encHandle ); + + fclose($encHandle); + + $fakeRoot = $this->view->getRoot(); + $this->view->chroot('/'.$this->userId.'/files'); + + $this->view->rename($relPath . '.tmp', $relPath); + + $this->view->chroot($fakeRoot); + // Add the file to the cache - \OC\Files\Filesystem::putFileInfo( $plainFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' ); - + \OC\Files\Filesystem::putFileInfo( $relPath, array( 'encrypted' => true, 'size' => $size, 'unencrypted_size' => $size ) ); } - + // Encrypt legacy encrypted files - if ( - ! empty( $legacyPassphrase ) - && ! empty( $newPassphrase ) + if ( + !empty($legacyPassphrase) + && !empty($newPassphrase) ) { - - foreach ( $found['legacy'] as $legacyFile ) { - + + foreach ($found['legacy'] as $legacyFile) { + // Fetch data from file - $legacyData = $this->view->file_get_contents( $legacyFile['path'] ); - + $legacyData = $this->view->file_get_contents($legacyFile['path']); + + $sharingEnabled = \OCP\Share::isEnabled(); + + // if file exists try to get sharing users + if ($this->view->file_exists($legacyFile['path'])) { + $uniqueUserIds = $this->getSharingUsersArray($sharingEnabled, $legacyFile['path'], $this->userId); + } else { + $uniqueUserIds[] = $this->userId; + } + + // Fetch public keys for all users who will share the file + $publicKeys = Keymanager::getPublicKeys($this->view, $uniqueUserIds); + // Recrypt data, generate catfile - $recrypted = Crypt::legacyKeyRecryptKeyfile( $legacyData, $legacyPassphrase, $publicKey, $newPassphrase ); - - $relPath = $this->stripUserFilesPath( $legacyFile['path'] ); - + $recrypted = Crypt::legacyKeyRecryptKeyfile( $legacyData, $legacyPassphrase, $publicKeys ); + + $rawPath = $legacyFile['path']; + $relPath = \OCA\Encryption\Helper::stripUserFilesPath($rawPath); + // Save keyfile - Keymanager::setFileKey( $this->view, $relPath, $this->userId, $recrypted['key'] ); - + Keymanager::setFileKey($this->view, $relPath, $this->userId, $recrypted['filekey']); + + // Save sharekeys to user folders + Keymanager::setShareKeys($this->view, $relPath, $recrypted['sharekeys']); + // Overwrite the existing file with the encrypted one - $this->view->file_put_contents( $legacyFile['path'], $recrypted['data'] ); - - $size = strlen( $recrypted['data'] ); - + $this->view->file_put_contents($rawPath, $recrypted['data']); + + $size = strlen($recrypted['data']); + // Add the file to the cache - \OC\Files\Filesystem::putFileInfo( $legacyFile['path'], array( 'encrypted'=>true, 'size' => $size ), '' ); - + \OC\Files\Filesystem::putFileInfo($rawPath, array( + 'encrypted' => true, + 'size' => $size + ), ''); } - } - + \OC_FileProxy::$enabled = true; - + // If files were found, return true return true; - } else { - + // If no files were found, return false return false; - } - } - + /** * @brief 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 ) { - + public function getPath($pathName) { + + switch ($pathName) { + case 'publicKeyDir': - + return $this->publicKeyDir; - + break; - + case 'encryptionDir': - + return $this->encryptionDir; - + break; - + case 'keyfilesPath': - + return $this->keyfilesPath; - + break; - + case 'publicKeyPath': - + return $this->publicKeyPath; - + break; - + case 'privateKeyPath': - + return $this->privateKeyPath; - + break; - } - + + return false; + + } + + /** + * @brief get path of a file. + * @param int $fileId id of the file + * @return string path of the file + */ + public static function fileIdToPath($fileId) { + + $sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?'; + + $query = \OCP\DB::prepare($sql); + + $result = $query->execute(array($fileId)); + + $path = false; + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $row = $result->fetchRow(); + $path = substr($row['path'], strlen('files')); + } + } + + return $path; + + } + + /** + * @brief 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) { + + $util = new Util($this->view, $user); + + // Check that the user is encryption capable, or is the + // public system user 'ownCloud' (for public shares) + if ( + $user === $this->publicShareKeyId + or $user === $this->recoveryKeyId + or $util->ready() + ) { + + // 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 + ); + + } + + /** + * @brief Decrypt a keyfile without knowing how it was encrypted + * @param string $filePath + * @param string $fileOwner + * @param string $privateKey + * @return bool|string + * @note Checks whether file was encrypted with openssl_seal or + * openssl_encrypt, and decrypts accrdingly + * @note This was used when 2 types of encryption for keyfiles was used, + * but now we've switched to exclusively using openssl_seal() + */ + public function decryptUnknownKeyfile($filePath, $fileOwner, $privateKey) { + + // Get the encrypted keyfile + // NOTE: the keyfile format depends on how it was encrypted! At + // this stage we don't know how it was encrypted + $encKeyfile = Keymanager::getFileKey($this->view, $this->userId, $filePath); + + // We need to decrypt the keyfile + // Has the file been shared yet? + if ( + $this->userId === $fileOwner + && !Keymanager::getShareKey($this->view, $this->userId, $filePath) // NOTE: we can't use isShared() here because it's a post share hook so it always returns true + ) { + + // The file has no shareKey, and its keyfile must be + // decrypted conventionally + $plainKeyfile = Crypt::keyDecrypt($encKeyfile, $privateKey); + + + } else { + + // The file has a shareKey and must use it for decryption + $shareKey = Keymanager::getShareKey($this->view, $this->userId, $filePath); + + $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); + + } + + return $plainKeyfile; + + } + + /** + * @brief 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(); + + $fileOwner = \OC\Files\Filesystem::getOwner($filePath); + + // Decrypt keyfile + $plainKeyfile = $this->decryptUnknownKeyfile($filePath, $fileOwner, $privateKey); + + // Re-enc keyfile to (additional) sharekeys + $multiEncKey = Crypt::multiKeyEncrypt($plainKeyfile, $userPubKeys); + + // Save the recrypted key to it's owner's keyfiles directory + // Save new sharekeys to all necessary user directory + if ( + !Keymanager::setFileKey($this->view, $filePath, $fileOwner, $multiEncKey['data']) + || !Keymanager::setShareKeys($this->view, $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; + } + + /** + * @brief Find, sanitise and format users sharing a file + * @note This wraps other methods into a portable bundle + */ + public function getSharingUsersArray($sharingEnabled, $filePath, $currentUserId = false) { + + // Check if key recovery is enabled + if ( + \OC_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); + + $userIds = array(); + if ($sharingEnabled) { + + // Find out who, if anyone, is sharing the file + $result = \OCP\Share::getUsersSharingFile($ownerPath, $owner, true); + $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 = \OC_Appconfig::getValue('files_encryption', 'recoveryKeyId'); + + // Add recoveryAdmin to list of users sharing + $userIds[] = $recoveryKeyId; + + } + + // add current user if given + if ($currentUserId !== false) { + + $userIds[] = $currentUserId; + + } + + // Remove duplicate UIDs + $uniqueUserIds = array_unique($userIds); + + return $uniqueUserIds; + + } + + /** + * @brief Set file migration status for user + * @param $status + * @return bool + */ + public function setMigrationStatus($status) { + + $sql = 'UPDATE `*PREFIX*encryption` SET migration_status = ? WHERE uid = ?'; + + $args = array( + $status, + $this->userId + ); + + $query = \OCP\DB::prepare($sql); + + if ($query->execute($args)) { + + return true; + + } else { + + return false; + + } + + } + + /** + * @brief 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 getMigrationStatus() { + + $sql = 'SELECT `migration_status` FROM `*PREFIX*encryption` WHERE uid = ?'; + + $args = array($this->userId); + + $query = \OCP\DB::prepare($sql); + + $result = $query->execute($args); + + $migrationStatus = array(); + + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $row = $result->fetchRow(); + if (isset($row['migration_status'])) { + $migrationStatus[] = $row['migration_status']; + } + } + } + + // If no record is found + if (empty($migrationStatus)) { + + return false; + + // If a record is found + } else { + + return $migrationStatus[0]; + + } + + } + + /** + * @brief 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) { + + $view = new \OC\Files\View($this->userFilesDir); + $fileOwnerUid = $view->getOwner($path); + + // handle public access + if ($this->isPublic) { + $filename = $path; + $fileOwnerUid = $GLOBALS['fileOwner']; + + return array( + $fileOwnerUid, + $filename + ); + } 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($path); + $ownerView = new \OC\Files\View('/' . $fileOwnerUid . '/files'); + + // Fetch real file path from DB + $filename = $ownerView->getPath($info['fileid']); // TODO: Check that this returns a path without including the user data dir + + } + + return array( + $fileOwnerUid, + $filename + ); + } + + + } + + /** + * @brief 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) { + + $result = array(); + + $content = $this->view->getDirectoryContent(\OC\Files\Filesystem::normalizePath($this->userFilesDir . '/' . $dir)); + + // handling for re shared folders + $pathSplit = explode('/', $dir); + + foreach ($content as $c) { + + $sharedPart = $pathSplit[sizeof($pathSplit) - 1]; + $targetPathSplit = array_reverse(explode('/', $c['path'])); + + $path = ''; + + // rebuild path + foreach ($targetPathSplit as $pathPart) { + + if ($pathPart !== $sharedPart) { + + $path = '/' . $pathPart . $path; + + } else { + + break; + + } + + } + + $path = $dir . $path; + + if ($c['type'] === 'dir') { + + $result = array_merge($result, $this->getAllFiles($path)); + + } else { + + $result[] = $path; + + } + } + + return $result; + + } + + /** + * @brief get shares parent. + * @param int $id of the current share + * @return array of the parent + */ + public static function getShareParent($id) { + + $sql = 'SELECT `file_target`, `item_type` FROM `*PREFIX*share` WHERE `id` = ?'; + + $query = \OCP\DB::prepare($sql); + + $result = $query->execute(array($id)); + + $row = array(); + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $row = $result->fetchRow(); + } + } + + return $row; + + } + + /** + * @brief get shares parent. + * @param int $id of the current share + * @return array of the parent + */ + public static function getParentFromShare($id) { + + $sql = 'SELECT `parent` FROM `*PREFIX*share` WHERE `id` = ?'; + + $query = \OCP\DB::prepare($sql); + + $result = $query->execute(array($id)); + + $row = array(); + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $row = $result->fetchRow(); + } + } + + return $row; + + } + + /** + * @brief get owner of the shared files. + * @param $id + * @internal param int $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 = array(); + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $source = $result->fetchRow(); + } + } + + $fileOwner = false; + + if (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 = array(); + if (\OCP\DB::isError($result)) { + \OCP\Util::writeLog('Encryption library', \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + } else { + if($result->numRows() > 0) { + $item = $result->fetchRow(); + } + } + + if (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 getUserFilesDir() { + return $this->userFilesDir; + } + + /** + * @param $password + * @return bool + */ + public function checkRecoveryPassword($password) { + + $pathKey = '/owncloud_private_key/' . $this->recoveryKeyId . ".private.key"; + $pathControlData = '/control-file/controlfile.enc'; + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $recoveryKey = $this->view->file_get_contents($pathKey); + + $decryptedRecoveryKey = Crypt::symmetricDecryptFileContent($recoveryKey, $password); + + $controlData = $this->view->file_get_contents($pathControlData); + $decryptedControlData = Crypt::keyDecrypt($controlData, $decryptedRecoveryKey); + + \OC_FileProxy::$enabled = $proxyStatus; + + if ($decryptedControlData === 'ownCloud') { + return true; + } + + return false; + } + + /** + * @return string + */ + public function getRecoveryKeyId() { + return $this->recoveryKeyId; + } + + /** + * @brief add recovery key to all encrypted files + */ + public function addRecoveryKeys($path = '/') { + $dirContent = $this->view->getDirectoryContent($this->keyfilesPath . $path); + foreach ($dirContent as $item) { + // get relative path from files_encryption/keyfiles/ + $filePath = substr($item['path'], strlen('files_encryption/keyfiles')); + if ($item['type'] === 'dir') { + $this->addRecoveryKeys($filePath . '/'); + } else { + $session = new \OCA\Encryption\Session(new \OC_FilesystemView('/')); + $sharingEnabled = \OCP\Share::isEnabled(); + // remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt' + $file = substr($filePath, 0, -4); + $usersSharing = $this->getSharingUsersArray($sharingEnabled, $file); + $this->setSharedFileKeyfiles($session, $usersSharing, $file); + } + } + } + + /** + * @brief remove recovery key to all encrypted files + */ + public function removeRecoveryKeys($path = '/') { + $dirContent = $this->view->getDirectoryContent($this->keyfilesPath . $path); + foreach ($dirContent as $item) { + // get relative path from files_encryption/keyfiles + $filePath = substr($item['path'], strlen('files_encryption/keyfiles')); + if ($item['type'] === 'dir') { + $this->removeRecoveryKeys($filePath . '/'); + } else { + // remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt' + $file = substr($filePath, 0, -4); + $this->view->unlink($this->shareKeysPath . '/' . $file . '.' . $this->recoveryKeyId . '.shareKey'); + } + } + } + + /** + * @brief 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); + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + //decrypt file key + $encKeyfile = $this->view->file_get_contents($this->keyfilesPath . $file . ".key"); + $shareKey = $this->view->file_get_contents( + $this->shareKeysPath . $file . "." . $this->recoveryKeyId . ".shareKey"); + $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); + + // write new keys to filesystem TDOO! + $this->view->file_put_contents($this->keyfilesPath . $file . '.key', $multiEncKey['data']); + foreach ($multiEncKey['keys'] as $userId => $shareKey) { + $shareKeyPath = $this->shareKeysPath . $file . '.' . $userId . '.shareKey'; + $this->view->file_put_contents($shareKeyPath, $shareKey); + } + + // Return proxy to original status + \OC_FileProxy::$enabled = $proxyStatus; + } + + /** + * @brief 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->keyfilesPath . $path); + foreach ($dirContent as $item) { + // get relative path from files_encryption/keyfiles + $filePath = substr($item['path'], strlen('files_encryption/keyfiles')); + if ($item['type'] === 'dir') { + $this->recoverAllFiles($filePath . '/', $privateKey); + } else { + // remove '.key' extension from path e.g. 'file.txt.key' to 'file.txt' + $file = substr($filePath, 0, -4); + $this->recoverFile($file, $privateKey); + } + } + } + + /** + * @brief recover users files in case of password lost + * @param string $recoveryPassword + */ + public function recoverUsersFiles($recoveryPassword) { + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $encryptedKey = $this->view->file_get_contents( + '/owncloud_private_key/' . $this->recoveryKeyId . '.private.key'); + $privateKey = Crypt::symmetricDecryptFileContent($encryptedKey, $recoveryPassword); + + \OC_FileProxy::$enabled = $proxyStatus; + + $this->recoverAllFiles('/', $privateKey); + } + + /** + * Get the path including the storage mount point + * @param int $id + * @return string the path including the mount point like AmazonS3/folder/file.txt + */ + public function getPathWithMountPoint($id) { + list($storage, $internalPath) = \OC\Files\Cache\Cache::getById($id); + $mount = \OC\Files\Filesystem::getMountByStorageId($storage); + $mountPoint = $mount[0]->getMountPoint(); + $path = \OC\Files\Filesystem::normalizePath($mountPoint.'/'.$internalPath); + + // reformat the path to be relative e.g. /user/files/folder becomes /folder/ + $relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path); + + return $relativePath; } } |