<commit_before><?php
/**
 * ownCloud
 *
 * @author <NAME>, <NAME>
 * @copyright 2012 <NAME> <<EMAIL>>,
 * <NAME> <<EMAIL>>
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

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 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_FilesystemView 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 $keyfilesPath; // Dir containing user's keyfiles
	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) {

		$this->view = $view;
		$this->client = $client;
		$this->userId = $userId;

		$this->publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId');
		$this->recoveryKeyId = \OC_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 = '/' . '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
		// make sure that the owners home is mounted
		\OC\Files\Filesystem::initMountPoints($userId);

		if (\OCA\Encryption\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->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 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;
		}
	}

	/**
	 * @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
		// 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);

				// Save key-pair
				if ($encryptedPrivateKey) {
					$this->view->file_put_contents($this->privateKeyPath, $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;
				}
			}
		}

		// 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`,`migration_status`) VALUES (?,?,?,?)';
			$args = array(
				$this->userId,
				'server-side',
				0,
				self::MIGRATION_OPEN
			);
			$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
			);

		}

		return is_numeric(\OC_DB::executeAudited($sql, $args));

	}

	/**
	 * @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 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(),
				'legacy' => 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 = \OCA\Encryption\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 (
								Keymanager::getFileKey($this->view, $this, $relPath)
								&& $isEncryptedPath
							) {

								$found['encrypted'][] = array(
									'name' => $file,
									'path' => $filePath
								);

								// If the file uses old
								// encryption system
							} elseif (Crypt::isLegacyEncryptedContent($isEncryptedPath, $relPath)) {

								$found['legacy'][] = array(
									'name' => $file,
									'path' => $filePath
								);

								// If the file is not encrypted
							} else {

								$found['plain'][] = array(
									'name' => $file,
									'path' => $relPath
								);

							}

						}

					}

				}
			}

			\OC_FileProxy::$enabled = true;

			if (empty($found)) {

				return false;

			} else {

				return $found;

			}

		}

		\OC_FileProxy::$enabled = true;

		return false;

	}

	/**
	 * @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;

		$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 $text;
	}

	/**
	 * @brief Check if a given path identifies an encrypted file
	 * @param string $path
	 * @return boolean
	 */
	public function isEncryptedPath($path) {

		$relPath = Helper::getPathToRealFile($path);

		if ($relPath === false) {
			$relPath = Helper::stripUserFilesPath($path);
		}

		$fileKey = Keymanager::getFileKey($this->view, $this, $relPath);

		if ($fileKey === false) {
			return false;
		}

		return true;

	}

	/**
	 * @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);

		if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path)
			&& $this->isEncryptedPath($path)
		) {

			// get the size from filesystem
			$size = $this->view->filesize($path);

			// 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/ 8192) - 1;
			$lastChunkSize = $size - ($lastChunkNr * 8192);

			// open stream
			$stream = fopen('crypt://' . $path, "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, $lastChunkSize);

				// 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 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;
	}

	/**
	 * @brief 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;
	}

	/**
	 * @brief 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/<user>/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' => $size,
					'etag' => $fileInfo['etag']
				));

				$decryptedFiles[] = $relPath;

			}

			if ($versionStatus) {
				\OC_App::enable('files_versions');
			}

			if (!$this->decryptVersions($decryptedFiles)) {
				$successful = false;
			}

			if ($successful) {
				$this->view->deleteAll($this->keyfilesPath);
				$this->view->deleteAll($this->shareKeysPath);
			}

			\OC_FileProxy::$enabled = true;<commit_msg>set unencrypted_size to 0 after decryption... so that the unencrypted_size gets re-calculated if encryption was enabled again<commit_after><?php
/**
 * ownCloud
 *
 * @author <NAME>, <NAME>
 * @copyright 2012 <NAME> <<EMAIL>>,
 * <NAME> <<EMAIL>>
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

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 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_FilesystemView 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 $keyfilesPath; // Dir containing user's keyfiles
	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) {

		$this->view = $view;
		$this->client = $client;
		$this->userId = $userId;

		$this->publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId');
		$this->recoveryKeyId = \OC_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 = '/' . '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
		// make sure that the owners home is mounted
		\OC\Files\Filesystem::initMountPoints($userId);

		if (\OCA\Encryption\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->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 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;
		}
	}

	/**
	 * @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
		// 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);

				// Save key-pair
				if ($encryptedPrivateKey) {
					$this->view->file_put_contents($this->privateKeyPath, $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;
				}
			}
		}

		// 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`,`migration_status`) VALUES (?,?,?,?)';
			$args = array(
				$this->userId,
				'server-side',
				0,
				self::MIGRATION_OPEN
			);
			$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
			);

		}

		return is_numeric(\OC_DB::executeAudited($sql, $args));

	}

	/**
	 * @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 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(),
				'legacy' => 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 = \OCA\Encryption\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 (
								Keymanager::getFileKey($this->view, $this, $relPath)
								&& $isEncryptedPath
							) {

								$found['encrypted'][] = array(
									'name' => $file,
									'path' => $filePath
								);

								// If the file uses old
								// encryption system
							} elseif (Crypt::isLegacyEncryptedContent($isEncryptedPath, $relPath)) {

								$found['legacy'][] = array(
									'name' => $file,
									'path' => $filePath
								);

								// If the file is not encrypted
							} else {

								$found['plain'][] = array(
									'name' => $file,
									'path' => $relPath
								);

							}

						}

					}

				}
			}

			\OC_FileProxy::$enabled = true;

			if (empty($found)) {

				return false;

			} else {

				return $found;

			}

		}

		\OC_FileProxy::$enabled = true;

		return false;

	}

	/**
	 * @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;

		$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 $text;
	}

	/**
	 * @brief Check if a given path identifies an encrypted file
	 * @param string $path
	 * @return boolean
	 */
	public function isEncryptedPath($path) {

		$relPath = Helper::getPathToRealFile($path);

		if ($relPath === false) {
			$relPath = Helper::stripUserFilesPath($path);
		}

		$fileKey = Keymanager::getFileKey($this->view, $this, $relPath);

		if ($fileKey === false) {
			return false;
		}

		return true;

	}

	/**
	 * @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);

		if (isset($pathParts[2]) && $pathParts[2] === 'files' && $this->view->file_exists($path)
			&& $this->isEncryptedPath($path)
		) {

			// get the size from filesystem
			$size = $this->view->filesize($path);

			// 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/ 8192) - 1;
			$lastChunkSize = $size - ($lastChunkNr * 8192);

			// open stream
			$stream = fopen('crypt://' . $path, "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, $lastChunkSize);

				// 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 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;
	}

	/**
	 * @brief 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;
	}

	/**
	 * @brief 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/<user>/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 ($successful) {
				$this->view->deleteAll($this->keyfilesPath);
				$this->view->deleteAll($this->shareKeysPath);
			}

			\OC_FileProxy::$enabled = true;
{'fraction_non_alphanumeric': 0.1586462839166339, 'fraction_numerical': 0.0027526543452615023, 'mean_word_length': 3.0202549155221816, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 373, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3498636', 'n_tokens_mistral': 15872, 'n_tokens_neox': 12929, 'n_words': 3401}