ocHeaderKeys = [ self::HEADER_ENCRYPTION_MODULE_KEY ]; $this->rootView = $rootView; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->config = $config; $this->excludedPaths[] = 'files_encryption'; $this->excludedPaths[] = 'appdata_' . $config->getSystemValueString('instanceid'); $this->excludedPaths[] = 'files_external'; } /** * read encryption module ID from header * * @param array $header * @return string * @throws ModuleDoesNotExistsException */ public function getEncryptionModuleId(?array $header = null) { $id = ''; $encryptionModuleKey = self::HEADER_ENCRYPTION_MODULE_KEY; if (isset($header[$encryptionModuleKey])) { $id = $header[$encryptionModuleKey]; } elseif (isset($header['cipher'])) { if (class_exists('\OCA\Encryption\Crypto\Encryption')) { // fall back to default encryption if the user migrated from // ownCloud <= 8.0 with the old encryption $id = \OCA\Encryption\Crypto\Encryption::ID; } else { throw new ModuleDoesNotExistsException('Default encryption module missing'); } } return $id; } /** * create header for encrypted file * * @param array $headerData * @param IEncryptionModule $encryptionModule * @return string * @throws EncryptionHeaderToLargeException if header has to many arguments * @throws EncryptionHeaderKeyExistsException if header key is already in use */ public function createHeader(array $headerData, IEncryptionModule $encryptionModule) { $header = self::HEADER_START . ':' . self::HEADER_ENCRYPTION_MODULE_KEY . ':' . $encryptionModule->getId() . ':'; foreach ($headerData as $key => $value) { if (in_array($key, $this->ocHeaderKeys)) { throw new EncryptionHeaderKeyExistsException($key); } $header .= $key . ':' . $value . ':'; } $header .= self::HEADER_END; if (strlen($header) > $this->getHeaderSize()) { throw new EncryptionHeaderToLargeException(); } $paddedHeader = str_pad($header, $this->headerSize, self::HEADER_PADDING_CHAR, STR_PAD_RIGHT); return $paddedHeader; } /** * 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 = []; $dirList = [$dir]; while ($dirList) { $dir = array_pop($dirList); $content = $this->rootView->getDirectoryContent($dir); foreach ($content as $c) { if ($c->getType() === 'dir') { $dirList[] = $c->getPath(); } else { $result[] = $c->getPath(); } } } return $result; } /** * check if it is a file uploaded by the user stored in data/user/files * or a metadata file * * @param string $path relative to the data/ folder * @return boolean */ public function isFile($path) { $parts = explode('/', Filesystem::normalizePath($path), 4); if (isset($parts[2]) && $parts[2] === 'files') { return true; } return false; } /** * return size of encryption header * * @return integer */ public function getHeaderSize() { return $this->headerSize; } /** * return size of block read by a PHP stream * * @return integer */ public function getBlockSize() { return $this->blockSize; } /** * get the owner and the path for the file relative to the owners files folder * * @param string $path * @return array{0: string, 1: string} * @throws \BadMethodCallException */ public function getUidAndFilename($path) { $parts = explode('/', $path); $uid = ''; if (count($parts) > 2) { $uid = $parts[1]; } if (!$this->userManager->userExists($uid)) { throw new \BadMethodCallException( 'path needs to be relative to the system wide data folder and point to a user specific file' ); } $ownerPath = implode('/', array_slice($parts, 2)); return [$uid, Filesystem::normalizePath($ownerPath)]; } /** * Remove .path extension from a file path * @param string $path Path that may identify a .part file * @return string File path without .part extension * @note this is needed for reusing keys */ public function stripPartialFileExtension($path) { $extension = pathinfo($path, PATHINFO_EXTENSION); if ($extension === 'part') { $newLength = strlen($path) - 5; // 5 = strlen(".part") $fPath = substr($path, 0, $newLength); // if path also contains a transaction id, we remove it too $extension = pathinfo($fPath, PATHINFO_EXTENSION); if (substr($extension, 0, 12) === 'ocTransferId') { // 12 = strlen("ocTransferId") $newLength = strlen($fPath) - strlen($extension) - 1; $fPath = substr($fPath, 0, $newLength); } return $fPath; } else { return $path; } } public function getUserWithAccessToMountPoint($users, $groups) { $result = []; if ($users === [] && $groups === []) { $users = $this->userManager->search('', null, null); $result = array_map(function (IUser $user) { return $user->getUID(); }, $users); } else { $result = array_merge($result, $users); $groupManager = $this->groupManager; foreach ($groups as $group) { $groupObject = $groupManager->get($group); if ($groupObject) { $foundUsers = $groupObject->searchUsers('', -1, 0); $userIds = []; foreach ($foundUsers as $user) { $userIds[] = $user->getUID(); } $result = array_merge($result, $userIds); } } } return $result; } /** * check if the file is stored on a system wide mount point * @param string $path relative to /data/user with leading '/' * @param string $uid * @return boolean */ public function isSystemWideMountPoint(string $path, string $uid) { $mount = Filesystem::getMountManager()->find('/' . $uid . $path); return $mount instanceof ISystemMountPoint; } /** * check if it is a path which is excluded by ownCloud from encryption * * @param string $path * @return boolean */ public function isExcluded($path) { $normalizedPath = Filesystem::normalizePath($path); $root = explode('/', $normalizedPath, 4); if (count($root) > 1) { // detect alternative key storage root $rootDir = $this->getKeyStorageRoot(); if ($rootDir !== '' && str_starts_with(Filesystem::normalizePath($path), Filesystem::normalizePath($rootDir)) ) { return true; } //detect system wide folders if (in_array($root[1], $this->excludedPaths)) { return true; } // detect user specific folders if ($this->userManager->userExists($root[1]) && in_array($root[2], $this->excludedPaths)) { return true; } } return false; } /** * Check if recovery key is enabled for user */ public function recoveryEnabled(string $uid): bool { $enabled = $this->config->getUserValue($uid, 'encryption', 'recovery_enabled', '0'); return $enabled === '1'; } /** * Set new key storage root * * @param string $root new key store root relative to the data folder */ public function setKeyStorageRoot(string $root): void { $this->config->setAppValue('core', 'encryption_key_storage_root', $root); } /** * Get key storage root * * @return string key storage root */ public function getKeyStorageRoot(): string { return $this->config->getAppValue('core', 'encryption_key_storage_root', ''); } /** * parse raw header to array * * @param string $rawHeader * @return array */ public function parseRawHeader(string $rawHeader) { $result = []; if (str_starts_with($rawHeader, Util::HEADER_START)) { $header = $rawHeader; $endAt = strpos($header, Util::HEADER_END); if ($endAt !== false) { $header = substr($header, 0, $endAt + strlen(Util::HEADER_END)); // +1 to not start with an ':' which would result in empty element at the beginning $exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1)); $element = array_shift($exploded); while ($element !== Util::HEADER_END && $element !== null) { $result[$element] = array_shift($exploded); $element = array_shift($exploded); } } } return $result; } /** * get path to key folder for a given file * * @param string $encryptionModuleId * @param string $path path to the file, relative to data/ * @return string */ public function getFileKeyDir(string $encryptionModuleId, string $path): string { [$owner, $filename] = $this->getUidAndFilename($path); $root = $this->getKeyStorageRoot(); // in case of system-wide mount points the keys are stored directly in the data directory if ($this->isSystemWideMountPoint($filename, $owner)) { $keyPath = $root . '/' . '/files_encryption/keys' . $filename . '/'; } else { $keyPath = $root . '/' . $owner . '/files_encryption/keys' . $filename . '/'; } return Filesystem::normalizePath($keyPath . $encryptionModuleId . '/', false); } }