1
0

KeyManager.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Encryption;
  8. use OC\Encryption\Exceptions\DecryptionFailedException;
  9. use OC\Files\View;
  10. use OCA\Encryption\Crypto\Crypt;
  11. use OCA\Encryption\Crypto\Encryption;
  12. use OCA\Encryption\Exceptions\PrivateKeyMissingException;
  13. use OCA\Encryption\Exceptions\PublicKeyMissingException;
  14. use OCP\Encryption\Keys\IStorage;
  15. use OCP\IConfig;
  16. use OCP\IUserSession;
  17. use OCP\Lock\ILockingProvider;
  18. use Psr\Log\LoggerInterface;
  19. class KeyManager {
  20. private string $recoveryKeyId;
  21. private string $publicShareKeyId;
  22. private string $masterKeyId;
  23. private string $keyId;
  24. private string $publicKeyId = 'publicKey';
  25. private string $privateKeyId = 'privateKey';
  26. private string $shareKeyId = 'shareKey';
  27. private string $fileKeyId = 'fileKey';
  28. public function __construct(
  29. private IStorage $keyStorage,
  30. private Crypt $crypt,
  31. private IConfig $config,
  32. IUserSession $userSession,
  33. private Session $session,
  34. private LoggerInterface $logger,
  35. private Util $util,
  36. private ILockingProvider $lockingProvider,
  37. ) {
  38. $this->recoveryKeyId = $this->config->getAppValue('encryption',
  39. 'recoveryKeyId');
  40. if (empty($this->recoveryKeyId)) {
  41. $this->recoveryKeyId = 'recoveryKey_' . substr(md5((string)time()), 0, 8);
  42. $this->config->setAppValue('encryption',
  43. 'recoveryKeyId',
  44. $this->recoveryKeyId);
  45. }
  46. $this->publicShareKeyId = $this->config->getAppValue('encryption',
  47. 'publicShareKeyId');
  48. if (empty($this->publicShareKeyId)) {
  49. $this->publicShareKeyId = 'pubShare_' . substr(md5((string)time()), 0, 8);
  50. $this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
  51. }
  52. $this->masterKeyId = $this->config->getAppValue('encryption',
  53. 'masterKeyId');
  54. if (empty($this->masterKeyId)) {
  55. $this->masterKeyId = 'master_' . substr(md5((string)time()), 0, 8);
  56. $this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
  57. }
  58. $this->keyId = $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false;
  59. }
  60. /**
  61. * check if key pair for public link shares exists, if not we create one
  62. */
  63. public function validateShareKey() {
  64. $shareKey = $this->getPublicShareKey();
  65. if (empty($shareKey)) {
  66. $this->lockingProvider->acquireLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: shared key generation');
  67. try {
  68. $keyPair = $this->crypt->createKeyPair();
  69. // Save public key
  70. $this->keyStorage->setSystemUserKey(
  71. $this->publicShareKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
  72. Encryption::ID);
  73. // Encrypt private key empty passphrase
  74. $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
  75. $header = $this->crypt->generateHeader();
  76. $this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
  77. } catch (\Throwable $e) {
  78. $this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
  79. throw $e;
  80. }
  81. $this->lockingProvider->releaseLock('encryption-generateSharedKey', ILockingProvider::LOCK_EXCLUSIVE);
  82. }
  83. }
  84. /**
  85. * check if a key pair for the master key exists, if not we create one
  86. */
  87. public function validateMasterKey() {
  88. if ($this->util->isMasterKeyEnabled() === false) {
  89. return;
  90. }
  91. $publicMasterKey = $this->getPublicMasterKey();
  92. $privateMasterKey = $this->getPrivateMasterKey();
  93. if (empty($publicMasterKey) && empty($privateMasterKey)) {
  94. // There could be a race condition here if two requests would trigger
  95. // the generation the second one would enter the key generation as long
  96. // as the first one didn't write the key to the keystorage yet
  97. $this->lockingProvider->acquireLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE, 'Encryption: master key generation');
  98. try {
  99. $keyPair = $this->crypt->createKeyPair();
  100. // Save public key
  101. $this->keyStorage->setSystemUserKey(
  102. $this->masterKeyId . '.' . $this->publicKeyId, $keyPair['publicKey'],
  103. Encryption::ID);
  104. // Encrypt private key with system password
  105. $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
  106. $header = $this->crypt->generateHeader();
  107. $this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
  108. } catch (\Throwable $e) {
  109. $this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
  110. throw $e;
  111. }
  112. $this->lockingProvider->releaseLock('encryption-generateMasterKey', ILockingProvider::LOCK_EXCLUSIVE);
  113. } elseif (empty($publicMasterKey)) {
  114. $this->logger->error('A private master key is available but the public key could not be found. This should never happen.');
  115. return;
  116. } elseif (empty($privateMasterKey)) {
  117. $this->logger->error('A public master key is available but the private key could not be found. This should never happen.');
  118. return;
  119. }
  120. if (!$this->session->isPrivateKeySet()) {
  121. $masterKey = $this->getSystemPrivateKey($this->masterKeyId);
  122. $decryptedMasterKey = $this->crypt->decryptPrivateKey($masterKey, $this->getMasterKeyPassword(), $this->masterKeyId);
  123. $this->session->setPrivateKey($decryptedMasterKey);
  124. }
  125. // after the encryption key is available we are ready to go
  126. $this->session->setStatus(Session::INIT_SUCCESSFUL);
  127. }
  128. /**
  129. * @return bool
  130. */
  131. public function recoveryKeyExists() {
  132. $key = $this->getRecoveryKey();
  133. return !empty($key);
  134. }
  135. /**
  136. * get recovery key
  137. *
  138. * @return string
  139. */
  140. public function getRecoveryKey() {
  141. return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->publicKeyId, Encryption::ID);
  142. }
  143. /**
  144. * get recovery key ID
  145. *
  146. * @return string
  147. */
  148. public function getRecoveryKeyId() {
  149. return $this->recoveryKeyId;
  150. }
  151. /**
  152. * @param string $password
  153. * @return bool
  154. */
  155. public function checkRecoveryPassword($password) {
  156. $recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.' . $this->privateKeyId, Encryption::ID);
  157. $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
  158. if ($decryptedRecoveryKey) {
  159. return true;
  160. }
  161. return false;
  162. }
  163. /**
  164. * @param string $uid
  165. * @param string $password
  166. * @param array $keyPair
  167. * @return bool
  168. */
  169. public function storeKeyPair($uid, $password, $keyPair) {
  170. // Save Public Key
  171. $this->setPublicKey($uid, $keyPair['publicKey']);
  172. $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $uid);
  173. $header = $this->crypt->generateHeader();
  174. if ($encryptedKey) {
  175. $this->setPrivateKey($uid, $header . $encryptedKey);
  176. return true;
  177. }
  178. return false;
  179. }
  180. /**
  181. * @param string $password
  182. * @param array $keyPair
  183. * @return bool
  184. */
  185. public function setRecoveryKey($password, $keyPair) {
  186. // Save Public Key
  187. $this->keyStorage->setSystemUserKey($this->getRecoveryKeyId() .
  188. '.' . $this->publicKeyId,
  189. $keyPair['publicKey'],
  190. Encryption::ID);
  191. $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password);
  192. $header = $this->crypt->generateHeader();
  193. if ($encryptedKey) {
  194. $this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
  195. return true;
  196. }
  197. return false;
  198. }
  199. /**
  200. * @param $userId
  201. * @param $key
  202. * @return bool
  203. */
  204. public function setPublicKey($userId, $key) {
  205. return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key, Encryption::ID);
  206. }
  207. /**
  208. * @param $userId
  209. * @param string $key
  210. * @return bool
  211. */
  212. public function setPrivateKey($userId, $key) {
  213. return $this->keyStorage->setUserKey($userId,
  214. $this->privateKeyId,
  215. $key,
  216. Encryption::ID);
  217. }
  218. /**
  219. * write file key to key storage
  220. *
  221. * @param string $path
  222. * @param string $key
  223. * @return boolean
  224. */
  225. public function setFileKey($path, $key) {
  226. return $this->keyStorage->setFileKey($path, $this->fileKeyId, $key, Encryption::ID);
  227. }
  228. /**
  229. * set all file keys (the file key and the corresponding share keys)
  230. *
  231. * @param string $path
  232. * @param array $keys
  233. */
  234. public function setAllFileKeys($path, $keys) {
  235. $this->setFileKey($path, $keys['data']);
  236. foreach ($keys['keys'] as $uid => $keyFile) {
  237. $this->setShareKey($path, $uid, $keyFile);
  238. }
  239. }
  240. /**
  241. * write share key to the key storage
  242. *
  243. * @param string $path
  244. * @param string $uid
  245. * @param string $key
  246. * @return boolean
  247. */
  248. public function setShareKey($path, $uid, $key) {
  249. $keyId = $uid . '.' . $this->shareKeyId;
  250. return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
  251. }
  252. /**
  253. * Decrypt private key and store it
  254. *
  255. * @return boolean
  256. */
  257. public function init(string $uid, ?string $passPhrase) {
  258. $this->session->setStatus(Session::INIT_EXECUTED);
  259. try {
  260. if ($this->util->isMasterKeyEnabled()) {
  261. $uid = $this->getMasterKeyId();
  262. $passPhrase = $this->getMasterKeyPassword();
  263. $privateKey = $this->getSystemPrivateKey($uid);
  264. } else {
  265. if ($passPhrase === null) {
  266. $this->logger->warning('Master key is disabled but not passphrase provided.');
  267. return false;
  268. }
  269. $privateKey = $this->getPrivateKey($uid);
  270. }
  271. $privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
  272. } catch (PrivateKeyMissingException $e) {
  273. return false;
  274. } catch (DecryptionFailedException $e) {
  275. return false;
  276. } catch (\Exception $e) {
  277. $this->logger->warning(
  278. 'Could not decrypt the private key from user "' . $uid . '"" during login. Assume password change on the user back-end.',
  279. [
  280. 'app' => 'encryption',
  281. 'exception' => $e,
  282. ]
  283. );
  284. return false;
  285. }
  286. if ($privateKey) {
  287. $this->session->setPrivateKey($privateKey);
  288. $this->session->setStatus(Session::INIT_SUCCESSFUL);
  289. return true;
  290. }
  291. return false;
  292. }
  293. /**
  294. * @param $userId
  295. * @return string
  296. * @throws PrivateKeyMissingException
  297. */
  298. public function getPrivateKey($userId) {
  299. $privateKey = $this->keyStorage->getUserKey($userId,
  300. $this->privateKeyId, Encryption::ID);
  301. if (strlen($privateKey) !== 0) {
  302. return $privateKey;
  303. }
  304. throw new PrivateKeyMissingException($userId);
  305. }
  306. /**
  307. * @param ?bool $useLegacyFileKey null means try both
  308. */
  309. public function getFileKey(string $path, ?string $uid, ?bool $useLegacyFileKey, bool $useDecryptAll = false): string {
  310. if ($uid === '') {
  311. $uid = null;
  312. }
  313. $publicAccess = is_null($uid);
  314. $encryptedFileKey = '';
  315. if ($useLegacyFileKey ?? true) {
  316. $encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID);
  317. if (empty($encryptedFileKey) && $useLegacyFileKey) {
  318. return '';
  319. }
  320. }
  321. if ($useDecryptAll) {
  322. $shareKey = $this->getShareKey($path, $this->session->getDecryptAllUid());
  323. $privateKey = $this->session->getDecryptAllKey();
  324. } elseif ($this->util->isMasterKeyEnabled()) {
  325. $uid = $this->getMasterKeyId();
  326. $shareKey = $this->getShareKey($path, $uid);
  327. if ($publicAccess) {
  328. $privateKey = $this->getSystemPrivateKey($uid);
  329. $privateKey = $this->crypt->decryptPrivateKey($privateKey, $this->getMasterKeyPassword(), $uid);
  330. } else {
  331. // when logged in, the master key is already decrypted in the session
  332. $privateKey = $this->session->getPrivateKey();
  333. }
  334. } elseif ($publicAccess) {
  335. // use public share key for public links
  336. $uid = $this->getPublicShareKeyId();
  337. $shareKey = $this->getShareKey($path, $uid);
  338. $privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->privateKeyId, Encryption::ID);
  339. $privateKey = $this->crypt->decryptPrivateKey($privateKey);
  340. } else {
  341. $shareKey = $this->getShareKey($path, $uid);
  342. $privateKey = $this->session->getPrivateKey();
  343. }
  344. if ($useLegacyFileKey ?? true) {
  345. if ($encryptedFileKey && $shareKey && $privateKey) {
  346. return $this->crypt->multiKeyDecryptLegacy($encryptedFileKey,
  347. $shareKey,
  348. $privateKey);
  349. }
  350. }
  351. if (!($useLegacyFileKey ?? false)) {
  352. if ($shareKey && $privateKey) {
  353. return $this->crypt->multiKeyDecrypt($shareKey, $privateKey);
  354. }
  355. }
  356. return '';
  357. }
  358. /**
  359. * Get the current version of a file
  360. *
  361. * @param string $path
  362. * @param View $view
  363. * @return int
  364. */
  365. public function getVersion($path, View $view) {
  366. $fileInfo = $view->getFileInfo($path);
  367. if ($fileInfo === false) {
  368. return 0;
  369. }
  370. return $fileInfo->getEncryptedVersion();
  371. }
  372. /**
  373. * Set the current version of a file
  374. *
  375. * @param string $path
  376. * @param int $version
  377. * @param View $view
  378. */
  379. public function setVersion($path, $version, View $view) {
  380. $fileInfo = $view->getFileInfo($path);
  381. if ($fileInfo !== false) {
  382. $cache = $fileInfo->getStorage()->getCache();
  383. $cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
  384. }
  385. }
  386. /**
  387. * get the encrypted file key
  388. *
  389. * @param string $path
  390. * @return string
  391. */
  392. public function getEncryptedFileKey($path) {
  393. $encryptedFileKey = $this->keyStorage->getFileKey($path,
  394. $this->fileKeyId, Encryption::ID);
  395. return $encryptedFileKey;
  396. }
  397. /**
  398. * delete share key
  399. *
  400. * @param string $path
  401. * @param string $keyId
  402. * @return boolean
  403. */
  404. public function deleteShareKey($path, $keyId) {
  405. return $this->keyStorage->deleteFileKey(
  406. $path,
  407. $keyId . '.' . $this->shareKeyId,
  408. Encryption::ID);
  409. }
  410. /**
  411. * @param $path
  412. * @param $uid
  413. * @return mixed
  414. */
  415. public function getShareKey($path, $uid) {
  416. $keyId = $uid . '.' . $this->shareKeyId;
  417. return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
  418. }
  419. /**
  420. * check if user has a private and a public key
  421. *
  422. * @param string $userId
  423. * @return bool
  424. * @throws PrivateKeyMissingException
  425. * @throws PublicKeyMissingException
  426. */
  427. public function userHasKeys($userId) {
  428. $privateKey = $publicKey = true;
  429. $exception = null;
  430. try {
  431. $this->getPrivateKey($userId);
  432. } catch (PrivateKeyMissingException $e) {
  433. $privateKey = false;
  434. $exception = $e;
  435. }
  436. try {
  437. $this->getPublicKey($userId);
  438. } catch (PublicKeyMissingException $e) {
  439. $publicKey = false;
  440. $exception = $e;
  441. }
  442. if ($privateKey && $publicKey) {
  443. return true;
  444. } elseif (!$privateKey && !$publicKey) {
  445. return false;
  446. } else {
  447. throw $exception;
  448. }
  449. }
  450. /**
  451. * @param $userId
  452. * @return mixed
  453. * @throws PublicKeyMissingException
  454. */
  455. public function getPublicKey($userId) {
  456. $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId, Encryption::ID);
  457. if (strlen($publicKey) !== 0) {
  458. return $publicKey;
  459. }
  460. throw new PublicKeyMissingException($userId);
  461. }
  462. public function getPublicShareKeyId() {
  463. return $this->publicShareKeyId;
  464. }
  465. /**
  466. * get public key for public link shares
  467. *
  468. * @return string
  469. */
  470. public function getPublicShareKey() {
  471. return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.' . $this->publicKeyId, Encryption::ID);
  472. }
  473. /**
  474. * @param string $purpose
  475. * @param string $uid
  476. */
  477. public function backupUserKeys($purpose, $uid) {
  478. $this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid);
  479. }
  480. /**
  481. * create a backup of the users private and public key and then delete it
  482. *
  483. * @param string $uid
  484. */
  485. public function deleteUserKeys($uid) {
  486. $this->deletePublicKey($uid);
  487. $this->deletePrivateKey($uid);
  488. }
  489. /**
  490. * @param $uid
  491. * @return bool
  492. */
  493. public function deletePublicKey($uid) {
  494. return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId, Encryption::ID);
  495. }
  496. /**
  497. * @param string $uid
  498. * @return bool
  499. */
  500. private function deletePrivateKey($uid) {
  501. return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
  502. }
  503. /**
  504. * @param string $path
  505. * @return bool
  506. */
  507. public function deleteAllFileKeys($path) {
  508. return $this->keyStorage->deleteAllFileKeys($path);
  509. }
  510. public function deleteLegacyFileKey(string $path): bool {
  511. return $this->keyStorage->deleteFileKey($path, $this->fileKeyId, Encryption::ID);
  512. }
  513. /**
  514. * @param array $userIds
  515. * @return array
  516. * @throws PublicKeyMissingException
  517. */
  518. public function getPublicKeys(array $userIds) {
  519. $keys = [];
  520. foreach ($userIds as $userId) {
  521. try {
  522. $keys[$userId] = $this->getPublicKey($userId);
  523. } catch (PublicKeyMissingException $e) {
  524. continue;
  525. }
  526. }
  527. return $keys;
  528. }
  529. /**
  530. * @param string $keyId
  531. * @return string returns openssl key
  532. */
  533. public function getSystemPrivateKey($keyId) {
  534. return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
  535. }
  536. /**
  537. * @param string $keyId
  538. * @param string $key
  539. * @return string returns openssl key
  540. */
  541. public function setSystemPrivateKey($keyId, $key) {
  542. return $this->keyStorage->setSystemUserKey(
  543. $keyId . '.' . $this->privateKeyId,
  544. $key,
  545. Encryption::ID);
  546. }
  547. /**
  548. * add system keys such as the public share key and the recovery key
  549. *
  550. * @param array $accessList
  551. * @param array $publicKeys
  552. * @param string $uid
  553. * @return array
  554. * @throws PublicKeyMissingException
  555. */
  556. public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
  557. if (!empty($accessList['public'])) {
  558. $publicShareKey = $this->getPublicShareKey();
  559. if (empty($publicShareKey)) {
  560. throw new PublicKeyMissingException($this->getPublicShareKeyId());
  561. }
  562. $publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
  563. }
  564. if ($this->recoveryKeyExists() &&
  565. $this->util->isRecoveryEnabledForUser($uid)) {
  566. $publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();
  567. }
  568. return $publicKeys;
  569. }
  570. /**
  571. * get master key password
  572. *
  573. * @return string
  574. * @throws \Exception
  575. */
  576. public function getMasterKeyPassword() {
  577. $password = $this->config->getSystemValue('secret');
  578. if (empty($password)) {
  579. throw new \Exception('Can not get secret from Nextcloud instance');
  580. }
  581. return $password;
  582. }
  583. /**
  584. * return master key id
  585. *
  586. * @return string
  587. */
  588. public function getMasterKeyId() {
  589. return $this->masterKeyId;
  590. }
  591. /**
  592. * get public master key
  593. *
  594. * @return string
  595. */
  596. public function getPublicMasterKey() {
  597. return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->publicKeyId, Encryption::ID);
  598. }
  599. /**
  600. * get public master key
  601. *
  602. * @return string
  603. */
  604. public function getPrivateMasterKey() {
  605. return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.' . $this->privateKeyId, Encryption::ID);
  606. }
  607. }