hooks.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Sam Tuke
  6. * @copyright 2012 Sam Tuke samtuke@owncloud.org
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OCA\Encryption;
  23. use OC\Files\Filesystem;
  24. /**
  25. * Class for hook specific logic
  26. */
  27. class Hooks {
  28. // TODO: use passphrase for encrypting private key that is separate to
  29. // the login password
  30. /**
  31. * @brief Startup encryption backend upon user login
  32. * @note This method should never be called for users using client side encryption
  33. */
  34. public static function login($params) {
  35. $l = new \OC_L10N('files_encryption');
  36. //check if all requirements are met
  37. if(!Helper::checkRequirements() || !Helper::checkConfiguration() ) {
  38. $error_msg = $l->t("Missing requirements.");
  39. $hint = $l->t('Please make sure that PHP 5.3.3 or newer is installed and that OpenSSL together with the PHP extension is enabled and configured properly. For now, the encryption app has been disabled.');
  40. \OC_App::disable('files_encryption');
  41. \OCP\Util::writeLog('Encryption library', $error_msg . ' ' . $hint, \OCP\Util::ERROR);
  42. \OCP\Template::printErrorPage($error_msg, $hint);
  43. }
  44. $view = new \OC_FilesystemView('/');
  45. // ensure filesystem is loaded
  46. if(!\OC\Files\Filesystem::$loaded) {
  47. \OC_Util::setupFS($params['uid']);
  48. }
  49. $util = new Util($view, $params['uid']);
  50. // setup user, if user not ready force relogin
  51. if (Helper::setupUser($util, $params['password']) === false) {
  52. return false;
  53. }
  54. $encryptedKey = Keymanager::getPrivateKey($view, $params['uid']);
  55. $privateKey = Crypt::decryptPrivateKey($encryptedKey, $params['password']);
  56. if ($privateKey === false) {
  57. \OCP\Util::writeLog('Encryption library', 'Private key for user "' . $params['uid']
  58. . '" is not valid! Maybe the user password was changed from outside if so please change it back to gain access', \OCP\Util::ERROR);
  59. }
  60. $session = new \OCA\Encryption\Session($view);
  61. $session->setPrivateKey($privateKey);
  62. // Check if first-run file migration has already been performed
  63. $ready = false;
  64. if ($util->getMigrationStatus() === Util::MIGRATION_OPEN) {
  65. $ready = $util->beginMigration();
  66. }
  67. // If migration not yet done
  68. if ($ready) {
  69. $userView = new \OC_FilesystemView('/' . $params['uid']);
  70. // Set legacy encryption key if it exists, to support
  71. // depreciated encryption system
  72. if (
  73. $userView->file_exists('encryption.key')
  74. && $encLegacyKey = $userView->file_get_contents('encryption.key')
  75. ) {
  76. $plainLegacyKey = Crypt::legacyDecrypt($encLegacyKey, $params['password']);
  77. $session->setLegacyKey($plainLegacyKey);
  78. }
  79. // Encrypt existing user files:
  80. // This serves to upgrade old versions of the encryption
  81. // app (see appinfo/spec.txt)
  82. if (
  83. $util->encryptAll('/' . $params['uid'] . '/' . 'files', $session->getLegacyKey(), $params['password'])
  84. ) {
  85. \OC_Log::write(
  86. 'Encryption library', 'Encryption of existing files belonging to "' . $params['uid'] . '" completed'
  87. , \OC_Log::INFO
  88. );
  89. }
  90. // Register successful migration in DB
  91. $util->finishMigration();
  92. }
  93. return true;
  94. }
  95. /**
  96. * @brief setup encryption backend upon user created
  97. * @note This method should never be called for users using client side encryption
  98. */
  99. public static function postCreateUser($params) {
  100. $view = new \OC_FilesystemView('/');
  101. $util = new Util($view, $params['uid']);
  102. Helper::setupUser($util, $params['password']);
  103. }
  104. /**
  105. * @brief cleanup encryption backend upon user deleted
  106. * @note This method should never be called for users using client side encryption
  107. */
  108. public static function postDeleteUser($params) {
  109. $view = new \OC_FilesystemView('/');
  110. // cleanup public key
  111. $publicKey = '/public-keys/' . $params['uid'] . '.public.key';
  112. // Disable encryption proxy to prevent recursive calls
  113. $proxyStatus = \OC_FileProxy::$enabled;
  114. \OC_FileProxy::$enabled = false;
  115. $view->unlink($publicKey);
  116. \OC_FileProxy::$enabled = $proxyStatus;
  117. }
  118. /**
  119. * @brief If the password can't be changed within ownCloud, than update the key password in advance.
  120. */
  121. public static function preSetPassphrase($params) {
  122. if ( ! \OC_User::canUserChangePassword($params['uid']) ) {
  123. self::setPassphrase($params);
  124. }
  125. }
  126. /**
  127. * @brief Change a user's encryption passphrase
  128. * @param array $params keys: uid, password
  129. */
  130. public static function setPassphrase($params) {
  131. // Only attempt to change passphrase if server-side encryption
  132. // is in use (client-side encryption does not have access to
  133. // the necessary keys)
  134. if (Crypt::mode() === 'server') {
  135. if ($params['uid'] === \OCP\User::getUser()) {
  136. $view = new \OC_FilesystemView('/');
  137. $session = new \OCA\Encryption\Session($view);
  138. // Get existing decrypted private key
  139. $privateKey = $session->getPrivateKey();
  140. // Encrypt private key with new user pwd as passphrase
  141. $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password']);
  142. // Save private key
  143. Keymanager::setPrivateKey($encryptedPrivateKey);
  144. // NOTE: Session does not need to be updated as the
  145. // private key has not changed, only the passphrase
  146. // used to decrypt it has changed
  147. } else { // admin changed the password for a different user, create new keys and reencrypt file keys
  148. $user = $params['uid'];
  149. $recoveryPassword = $params['recoveryPassword'];
  150. $newUserPassword = $params['password'];
  151. $view = new \OC_FilesystemView('/');
  152. // make sure that the users home is mounted
  153. \OC\Files\Filesystem::initMountPoints($user);
  154. $keypair = Crypt::createKeypair();
  155. // Disable encryption proxy to prevent recursive calls
  156. $proxyStatus = \OC_FileProxy::$enabled;
  157. \OC_FileProxy::$enabled = false;
  158. // Save public key
  159. $view->file_put_contents('/public-keys/' . $user . '.public.key', $keypair['publicKey']);
  160. // Encrypt private key empty passphrase
  161. $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword);
  162. // Save private key
  163. $view->file_put_contents(
  164. '/' . $user . '/files_encryption/' . $user . '.private.key', $encryptedPrivateKey);
  165. if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
  166. $util = new Util($view, $user);
  167. $util->recoverUsersFiles($recoveryPassword);
  168. }
  169. \OC_FileProxy::$enabled = $proxyStatus;
  170. }
  171. }
  172. }
  173. /*
  174. * @brief check if files can be encrypted to every user.
  175. */
  176. /**
  177. * @param $params
  178. */
  179. public static function preShared($params) {
  180. $l = new \OC_L10N('files_encryption');
  181. $users = array();
  182. $view = new \OC\Files\View('/public-keys/');
  183. switch ($params['shareType']) {
  184. case \OCP\Share::SHARE_TYPE_USER:
  185. $users[] = $params['shareWith'];
  186. break;
  187. case \OCP\Share::SHARE_TYPE_GROUP:
  188. $users = \OC_Group::usersInGroup($params['shareWith']);
  189. break;
  190. }
  191. $notConfigured = array();
  192. foreach ($users as $user) {
  193. if (!$view->file_exists($user . '.public.key')) {
  194. $notConfigured[] = $user;
  195. }
  196. }
  197. if (count($notConfigured) > 0) {
  198. $params['run'] = false;
  199. $params['error'] = $l->t('Following users are not set up for encryption:') . ' ' . join(', ' , $notConfigured);
  200. }
  201. }
  202. /**
  203. * @brief
  204. */
  205. public static function postShared($params) {
  206. // NOTE: $params has keys:
  207. // [itemType] => file
  208. // itemSource -> int, filecache file ID
  209. // [parent] =>
  210. // [itemTarget] => /13
  211. // shareWith -> string, uid of user being shared to
  212. // fileTarget -> path of file being shared
  213. // uidOwner -> owner of the original file being shared
  214. // [shareType] => 0
  215. // [shareWith] => test1
  216. // [uidOwner] => admin
  217. // [permissions] => 17
  218. // [fileSource] => 13
  219. // [fileTarget] => /test8
  220. // [id] => 10
  221. // [token] =>
  222. // [run] => whether emitting script should continue to run
  223. // TODO: Should other kinds of item be encrypted too?
  224. if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
  225. $view = new \OC_FilesystemView('/');
  226. $session = new \OCA\Encryption\Session($view);
  227. $userId = \OCP\User::getUser();
  228. $util = new Util($view, $userId);
  229. $path = $util->fileIdToPath($params['itemSource']);
  230. $share = $util->getParentFromShare($params['id']);
  231. //if parent is set, then this is a re-share action
  232. if ($share['parent'] !== null) {
  233. // get the parent from current share
  234. $parent = $util->getShareParent($params['parent']);
  235. // if parent is file the it is an 1:1 share
  236. if ($parent['item_type'] === 'file') {
  237. // prefix path with Shared
  238. $path = '/Shared' . $parent['file_target'];
  239. } else {
  240. // NOTE: parent is folder but shared was a file!
  241. // we try to rebuild the missing path
  242. // some examples we face here
  243. // user1 share folder1 with user2 folder1 has
  244. // the following structure
  245. // /folder1/subfolder1/subsubfolder1/somefile.txt
  246. // user2 re-share subfolder2 with user3
  247. // user3 re-share somefile.txt user4
  248. // so our path should be
  249. // /Shared/subfolder1/subsubfolder1/somefile.txt
  250. // while user3 is sharing
  251. if ($params['itemType'] === 'file') {
  252. // get target path
  253. $targetPath = $util->fileIdToPath($params['fileSource']);
  254. $targetPathSplit = array_reverse(explode('/', $targetPath));
  255. // init values
  256. $path = '';
  257. $sharedPart = ltrim($parent['file_target'], '/');
  258. // rebuild path
  259. foreach ($targetPathSplit as $pathPart) {
  260. if ($pathPart !== $sharedPart) {
  261. $path = '/' . $pathPart . $path;
  262. } else {
  263. break;
  264. }
  265. }
  266. // prefix path with Shared
  267. $path = '/Shared' . $parent['file_target'] . $path;
  268. } else {
  269. // prefix path with Shared
  270. $path = '/Shared' . $parent['file_target'] . $params['fileTarget'];
  271. }
  272. }
  273. }
  274. $sharingEnabled = \OCP\Share::isEnabled();
  275. // get the path including mount point only if not a shared folder
  276. if (strncmp($path, '/Shared', strlen('/Shared') !== 0)) {
  277. // get path including the the storage mount point
  278. $path = $util->getPathWithMountPoint($params['itemSource']);
  279. }
  280. // if a folder was shared, get a list of all (sub-)folders
  281. if ($params['itemType'] === 'folder') {
  282. $allFiles = $util->getAllFiles($path);
  283. } else {
  284. $allFiles = array($path);
  285. }
  286. foreach ($allFiles as $path) {
  287. $usersSharing = $util->getSharingUsersArray($sharingEnabled, $path);
  288. $util->setSharedFileKeyfiles($session, $usersSharing, $path);
  289. }
  290. }
  291. }
  292. /**
  293. * @brief
  294. */
  295. public static function postUnshare($params) {
  296. // NOTE: $params has keys:
  297. // [itemType] => file
  298. // [itemSource] => 13
  299. // [shareType] => 0
  300. // [shareWith] => test1
  301. // [itemParent] =>
  302. if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
  303. $view = new \OC_FilesystemView('/');
  304. $userId = \OCP\User::getUser();
  305. $util = new Util($view, $userId);
  306. $path = $util->fileIdToPath($params['itemSource']);
  307. // check if this is a re-share
  308. if ($params['itemParent']) {
  309. // get the parent from current share
  310. $parent = $util->getShareParent($params['itemParent']);
  311. // get target path
  312. $targetPath = $util->fileIdToPath($params['itemSource']);
  313. $targetPathSplit = array_reverse(explode('/', $targetPath));
  314. // init values
  315. $path = '';
  316. $sharedPart = ltrim($parent['file_target'], '/');
  317. // rebuild path
  318. foreach ($targetPathSplit as $pathPart) {
  319. if ($pathPart !== $sharedPart) {
  320. $path = '/' . $pathPart . $path;
  321. } else {
  322. break;
  323. }
  324. }
  325. // prefix path with Shared
  326. $path = '/Shared' . $parent['file_target'] . $path;
  327. }
  328. // for group shares get a list of the group members
  329. if ($params['shareType'] === \OCP\Share::SHARE_TYPE_GROUP) {
  330. $userIds = \OC_Group::usersInGroup($params['shareWith']);
  331. } else {
  332. if ($params['shareType'] === \OCP\Share::SHARE_TYPE_LINK) {
  333. $userIds = array($util->getPublicShareKeyId());
  334. } else {
  335. $userIds = array($params['shareWith']);
  336. }
  337. }
  338. // get the path including mount point only if not a shared folder
  339. if (strncmp($path, '/Shared', strlen('/Shared') !== 0)) {
  340. // get path including the the storage mount point
  341. $path = $util->getPathWithMountPoint($params['itemSource']);
  342. }
  343. // if we unshare a folder we need a list of all (sub-)files
  344. if ($params['itemType'] === 'folder') {
  345. $allFiles = $util->getAllFiles($path);
  346. } else {
  347. $allFiles = array($path);
  348. }
  349. foreach ($allFiles as $path) {
  350. // check if the user still has access to the file, otherwise delete share key
  351. $sharingUsers = $util->getSharingUsersArray(true, $path);
  352. // Unshare every user who no longer has access to the file
  353. $delUsers = array_diff($userIds, $sharingUsers);
  354. // delete share key
  355. Keymanager::delShareKey($view, $delUsers, $path);
  356. }
  357. }
  358. }
  359. /**
  360. * @brief after a file is renamed, rename its keyfile and share-keys also fix the file size and fix also the sharing
  361. * @param array with oldpath and newpath
  362. *
  363. * This function is connected to the rename signal of OC_Filesystem and adjust the name and location
  364. * of the stored versions along the actual file
  365. */
  366. public static function postRename($params) {
  367. // Disable encryption proxy to prevent recursive calls
  368. $proxyStatus = \OC_FileProxy::$enabled;
  369. \OC_FileProxy::$enabled = false;
  370. $view = new \OC_FilesystemView('/');
  371. $session = new \OCA\Encryption\Session($view);
  372. $userId = \OCP\User::getUser();
  373. $util = new Util($view, $userId);
  374. // Format paths to be relative to user files dir
  375. if ($util->isSystemWideMountPoint($params['oldpath'])) {
  376. $baseDir = 'files_encryption/';
  377. $oldKeyfilePath = $baseDir . 'keyfiles/' . $params['oldpath'];
  378. } else {
  379. $baseDir = $userId . '/' . 'files_encryption/';
  380. $oldKeyfilePath = $baseDir . 'keyfiles/' . $params['oldpath'];
  381. }
  382. if ($util->isSystemWideMountPoint($params['newpath'])) {
  383. $newKeyfilePath = $baseDir . 'keyfiles/' . $params['newpath'];
  384. } else {
  385. $newKeyfilePath = $baseDir . 'keyfiles/' . $params['newpath'];
  386. }
  387. // add key ext if this is not an folder
  388. if (!$view->is_dir($oldKeyfilePath)) {
  389. $oldKeyfilePath .= '.key';
  390. $newKeyfilePath .= '.key';
  391. // handle share-keys
  392. $localKeyPath = $view->getLocalFile($baseDir . 'share-keys/' . $params['oldpath']);
  393. $escapedPath = Helper::escapeGlobPattern($localKeyPath);
  394. $matches = glob($escapedPath . '*.shareKey');
  395. foreach ($matches as $src) {
  396. $dst = \OC\Files\Filesystem::normalizePath(str_replace($params['oldpath'], $params['newpath'], $src));
  397. // create destination folder if not exists
  398. if (!file_exists(dirname($dst))) {
  399. mkdir(dirname($dst), 0750, true);
  400. }
  401. rename($src, $dst);
  402. }
  403. } else {
  404. // handle share-keys folders
  405. $oldShareKeyfilePath = $baseDir . 'share-keys/' . $params['oldpath'];
  406. $newShareKeyfilePath = $baseDir . 'share-keys/' . $params['newpath'];
  407. // create destination folder if not exists
  408. if (!$view->file_exists(dirname($newShareKeyfilePath))) {
  409. $view->mkdir(dirname($newShareKeyfilePath), 0750, true);
  410. }
  411. $view->rename($oldShareKeyfilePath, $newShareKeyfilePath);
  412. }
  413. // Rename keyfile so it isn't orphaned
  414. if ($view->file_exists($oldKeyfilePath)) {
  415. // create destination folder if not exists
  416. if (!$view->file_exists(dirname($newKeyfilePath))) {
  417. $view->mkdir(dirname($newKeyfilePath), 0750, true);
  418. }
  419. $view->rename($oldKeyfilePath, $newKeyfilePath);
  420. }
  421. // build the path to the file
  422. $newPath = '/' . $userId . '/files' . $params['newpath'];
  423. $newPathRelative = $params['newpath'];
  424. if ($util->fixFileSize($newPath)) {
  425. // get sharing app state
  426. $sharingEnabled = \OCP\Share::isEnabled();
  427. // get users
  428. $usersSharing = $util->getSharingUsersArray($sharingEnabled, $newPathRelative);
  429. // update sharing-keys
  430. $util->setSharedFileKeyfiles($session, $usersSharing, $newPathRelative);
  431. }
  432. \OC_FileProxy::$enabled = $proxyStatus;
  433. }
  434. /**
  435. * set migration status back to '0' so that all new files get encrypted
  436. * if the app gets enabled again
  437. * @param array $params contains the app ID
  438. */
  439. public static function preDisable($params) {
  440. if ($params['app'] === 'files_encryption') {
  441. $query = \OC_DB::prepare('UPDATE `*PREFIX*encryption` SET `migration_status`=0');
  442. $query->execute();
  443. }
  444. }
  445. }