Util.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Thomas Müller <thomas.mueller@tmit.eu>
  12. *
  13. * @license AGPL-3.0
  14. *
  15. * This code is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License, version 3,
  17. * as published by the Free Software Foundation.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU Affero General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU Affero General Public License, version 3,
  25. * along with this program. If not, see <http://www.gnu.org/licenses/>
  26. *
  27. */
  28. namespace OC\Encryption;
  29. use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException;
  30. use OC\Encryption\Exceptions\EncryptionHeaderToLargeException;
  31. use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
  32. use OC\Files\Filesystem;
  33. use OC\Files\View;
  34. use OCA\Files_External\Lib\StorageConfig;
  35. use OCA\Files_External\Service\GlobalStoragesService;
  36. use OCP\Encryption\IEncryptionModule;
  37. use OCP\IConfig;
  38. use OCP\IUser;
  39. class Util {
  40. public const HEADER_START = 'HBEGIN';
  41. public const HEADER_END = 'HEND';
  42. public const HEADER_PADDING_CHAR = '-';
  43. public const HEADER_ENCRYPTION_MODULE_KEY = 'oc_encryption_module';
  44. /**
  45. * block size will always be 8192 for a PHP stream
  46. * @see https://bugs.php.net/bug.php?id=21641
  47. * @var integer
  48. */
  49. protected $headerSize = 8192;
  50. /**
  51. * block size will always be 8192 for a PHP stream
  52. * @see https://bugs.php.net/bug.php?id=21641
  53. * @var integer
  54. */
  55. protected $blockSize = 8192;
  56. /** @var View */
  57. protected $rootView;
  58. /** @var array */
  59. protected $ocHeaderKeys;
  60. /** @var \OC\User\Manager */
  61. protected $userManager;
  62. /** @var IConfig */
  63. protected $config;
  64. /** @var array paths excluded from encryption */
  65. protected $excludedPaths;
  66. /** @var \OC\Group\Manager $manager */
  67. protected $groupManager;
  68. /**
  69. *
  70. * @param View $rootView
  71. * @param \OC\User\Manager $userManager
  72. * @param \OC\Group\Manager $groupManager
  73. * @param IConfig $config
  74. */
  75. public function __construct(
  76. View $rootView,
  77. \OC\User\Manager $userManager,
  78. \OC\Group\Manager $groupManager,
  79. IConfig $config) {
  80. $this->ocHeaderKeys = [
  81. self::HEADER_ENCRYPTION_MODULE_KEY
  82. ];
  83. $this->rootView = $rootView;
  84. $this->userManager = $userManager;
  85. $this->groupManager = $groupManager;
  86. $this->config = $config;
  87. $this->excludedPaths[] = 'files_encryption';
  88. $this->excludedPaths[] = 'appdata_' . $config->getSystemValue('instanceid', null);
  89. $this->excludedPaths[] = 'files_external';
  90. }
  91. /**
  92. * read encryption module ID from header
  93. *
  94. * @param array $header
  95. * @return string
  96. * @throws ModuleDoesNotExistsException
  97. */
  98. public function getEncryptionModuleId(array $header = null) {
  99. $id = '';
  100. $encryptionModuleKey = self::HEADER_ENCRYPTION_MODULE_KEY;
  101. if (isset($header[$encryptionModuleKey])) {
  102. $id = $header[$encryptionModuleKey];
  103. } elseif (isset($header['cipher'])) {
  104. if (class_exists('\OCA\Encryption\Crypto\Encryption')) {
  105. // fall back to default encryption if the user migrated from
  106. // ownCloud <= 8.0 with the old encryption
  107. $id = \OCA\Encryption\Crypto\Encryption::ID;
  108. } else {
  109. throw new ModuleDoesNotExistsException('Default encryption module missing');
  110. }
  111. }
  112. return $id;
  113. }
  114. /**
  115. * create header for encrypted file
  116. *
  117. * @param array $headerData
  118. * @param IEncryptionModule $encryptionModule
  119. * @return string
  120. * @throws EncryptionHeaderToLargeException if header has to many arguments
  121. * @throws EncryptionHeaderKeyExistsException if header key is already in use
  122. */
  123. public function createHeader(array $headerData, IEncryptionModule $encryptionModule) {
  124. $header = self::HEADER_START . ':' . self::HEADER_ENCRYPTION_MODULE_KEY . ':' . $encryptionModule->getId() . ':';
  125. foreach ($headerData as $key => $value) {
  126. if (in_array($key, $this->ocHeaderKeys)) {
  127. throw new EncryptionHeaderKeyExistsException($key);
  128. }
  129. $header .= $key . ':' . $value . ':';
  130. }
  131. $header .= self::HEADER_END;
  132. if (strlen($header) > $this->getHeaderSize()) {
  133. throw new EncryptionHeaderToLargeException();
  134. }
  135. $paddedHeader = str_pad($header, $this->headerSize, self::HEADER_PADDING_CHAR, STR_PAD_RIGHT);
  136. return $paddedHeader;
  137. }
  138. /**
  139. * go recursively through a dir and collect all files and sub files.
  140. *
  141. * @param string $dir relative to the users files folder
  142. * @return array with list of files relative to the users files folder
  143. */
  144. public function getAllFiles($dir) {
  145. $result = [];
  146. $dirList = [$dir];
  147. while ($dirList) {
  148. $dir = array_pop($dirList);
  149. $content = $this->rootView->getDirectoryContent($dir);
  150. foreach ($content as $c) {
  151. if ($c->getType() === 'dir') {
  152. $dirList[] = $c->getPath();
  153. } else {
  154. $result[] = $c->getPath();
  155. }
  156. }
  157. }
  158. return $result;
  159. }
  160. /**
  161. * check if it is a file uploaded by the user stored in data/user/files
  162. * or a metadata file
  163. *
  164. * @param string $path relative to the data/ folder
  165. * @return boolean
  166. */
  167. public function isFile($path) {
  168. $parts = explode('/', Filesystem::normalizePath($path), 4);
  169. if (isset($parts[2]) && $parts[2] === 'files') {
  170. return true;
  171. }
  172. return false;
  173. }
  174. /**
  175. * return size of encryption header
  176. *
  177. * @return integer
  178. */
  179. public function getHeaderSize() {
  180. return $this->headerSize;
  181. }
  182. /**
  183. * return size of block read by a PHP stream
  184. *
  185. * @return integer
  186. */
  187. public function getBlockSize() {
  188. return $this->blockSize;
  189. }
  190. /**
  191. * get the owner and the path for the file relative to the owners files folder
  192. *
  193. * @param string $path
  194. * @return array{0: string, 1: string}
  195. * @throws \BadMethodCallException
  196. */
  197. public function getUidAndFilename($path) {
  198. $parts = explode('/', $path);
  199. $uid = '';
  200. if (count($parts) > 2) {
  201. $uid = $parts[1];
  202. }
  203. if (!$this->userManager->userExists($uid)) {
  204. throw new \BadMethodCallException(
  205. 'path needs to be relative to the system wide data folder and point to a user specific file'
  206. );
  207. }
  208. $ownerPath = implode('/', array_slice($parts, 2));
  209. return [$uid, Filesystem::normalizePath($ownerPath)];
  210. }
  211. /**
  212. * Remove .path extension from a file path
  213. * @param string $path Path that may identify a .part file
  214. * @return string File path without .part extension
  215. * @note this is needed for reusing keys
  216. */
  217. public function stripPartialFileExtension($path) {
  218. $extension = pathinfo($path, PATHINFO_EXTENSION);
  219. if ($extension === 'part') {
  220. $newLength = strlen($path) - 5; // 5 = strlen(".part")
  221. $fPath = substr($path, 0, $newLength);
  222. // if path also contains a transaction id, we remove it too
  223. $extension = pathinfo($fPath, PATHINFO_EXTENSION);
  224. if (substr($extension, 0, 12) === 'ocTransferId') { // 12 = strlen("ocTransferId")
  225. $newLength = strlen($fPath) - strlen($extension) - 1;
  226. $fPath = substr($fPath, 0, $newLength);
  227. }
  228. return $fPath;
  229. } else {
  230. return $path;
  231. }
  232. }
  233. public function getUserWithAccessToMountPoint($users, $groups) {
  234. $result = [];
  235. if ($users === [] && $groups === []) {
  236. $users = $this->userManager->search('', null, null);
  237. $result = array_map(function (IUser $user) {
  238. return $user->getUID();
  239. }, $users);
  240. } else {
  241. $result = array_merge($result, $users);
  242. $groupManager = \OC::$server->getGroupManager();
  243. foreach ($groups as $group) {
  244. $groupObject = $groupManager->get($group);
  245. if ($groupObject) {
  246. $foundUsers = $groupObject->searchUsers('', -1, 0);
  247. $userIds = [];
  248. foreach ($foundUsers as $user) {
  249. $userIds[] = $user->getUID();
  250. }
  251. $result = array_merge($result, $userIds);
  252. }
  253. }
  254. }
  255. return $result;
  256. }
  257. /**
  258. * check if the file is stored on a system wide mount point
  259. * @param string $path relative to /data/user with leading '/'
  260. * @param string $uid
  261. * @return boolean
  262. */
  263. public function isSystemWideMountPoint($path, $uid) {
  264. if (\OCP\App::isEnabled("files_external")) {
  265. /** @var GlobalStoragesService $storageService */
  266. $storageService = \OC::$server->get(GlobalStoragesService::class);
  267. $storages = $storageService->getAllStorages();
  268. foreach ($storages as $storage) {
  269. if (strpos($path, '/files/' . ltrim($storage->getMountPoint(), '/')) === 0) {
  270. if ($this->isMountPointApplicableToUser($storage, $uid)) {
  271. return true;
  272. }
  273. }
  274. }
  275. }
  276. return false;
  277. }
  278. /**
  279. * check if mount point is applicable to user
  280. *
  281. * @param StorageConfig $mount
  282. * @param string $uid
  283. * @return boolean
  284. */
  285. private function isMountPointApplicableToUser(StorageConfig $mount, string $uid) {
  286. if ($mount->getApplicableUsers() === [] && $mount->getApplicableGroups() === []) {
  287. // applicable for everyone
  288. return true;
  289. }
  290. // check if mount point is applicable for the user
  291. if (array_search($uid, $mount->getApplicableUsers()) !== false) {
  292. return true;
  293. }
  294. // check if mount point is applicable for group where the user is a member
  295. foreach ($mount->getApplicableGroups() as $gid) {
  296. if ($this->groupManager->isInGroup($uid, $gid)) {
  297. return true;
  298. }
  299. }
  300. return false;
  301. }
  302. /**
  303. * check if it is a path which is excluded by ownCloud from encryption
  304. *
  305. * @param string $path
  306. * @return boolean
  307. */
  308. public function isExcluded($path) {
  309. $normalizedPath = Filesystem::normalizePath($path);
  310. $root = explode('/', $normalizedPath, 4);
  311. if (count($root) > 1) {
  312. // detect alternative key storage root
  313. $rootDir = $this->getKeyStorageRoot();
  314. if ($rootDir !== '' &&
  315. 0 === strpos(
  316. Filesystem::normalizePath($path),
  317. Filesystem::normalizePath($rootDir)
  318. )
  319. ) {
  320. return true;
  321. }
  322. //detect system wide folders
  323. if (in_array($root[1], $this->excludedPaths)) {
  324. return true;
  325. }
  326. // detect user specific folders
  327. if ($this->userManager->userExists($root[1])
  328. && in_array($root[2], $this->excludedPaths)) {
  329. return true;
  330. }
  331. }
  332. return false;
  333. }
  334. /**
  335. * check if recovery key is enabled for user
  336. *
  337. * @param string $uid
  338. * @return boolean
  339. */
  340. public function recoveryEnabled($uid) {
  341. $enabled = $this->config->getUserValue($uid, 'encryption', 'recovery_enabled', '0');
  342. return $enabled === '1';
  343. }
  344. /**
  345. * set new key storage root
  346. *
  347. * @param string $root new key store root relative to the data folder
  348. */
  349. public function setKeyStorageRoot($root) {
  350. $this->config->setAppValue('core', 'encryption_key_storage_root', $root);
  351. }
  352. /**
  353. * get key storage root
  354. *
  355. * @return string key storage root
  356. */
  357. public function getKeyStorageRoot() {
  358. return $this->config->getAppValue('core', 'encryption_key_storage_root', '');
  359. }
  360. }