LoginFlowV2Service.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Core\Service;
  8. use OC\Authentication\Exceptions\PasswordlessTokenException;
  9. use OC\Authentication\Token\IProvider;
  10. use OC\Authentication\Token\IToken;
  11. use OC\Core\Data\LoginFlowV2Credentials;
  12. use OC\Core\Data\LoginFlowV2Tokens;
  13. use OC\Core\Db\LoginFlowV2;
  14. use OC\Core\Db\LoginFlowV2Mapper;
  15. use OC\Core\Exception\LoginFlowV2NotFoundException;
  16. use OCP\AppFramework\Db\DoesNotExistException;
  17. use OCP\AppFramework\Utility\ITimeFactory;
  18. use OCP\Authentication\Exceptions\InvalidTokenException;
  19. use OCP\IConfig;
  20. use OCP\Security\ICrypto;
  21. use OCP\Security\ISecureRandom;
  22. use Psr\Log\LoggerInterface;
  23. class LoginFlowV2Service {
  24. public function __construct(
  25. private LoginFlowV2Mapper $mapper,
  26. private ISecureRandom $random,
  27. private ITimeFactory $time,
  28. private IConfig $config,
  29. private ICrypto $crypto,
  30. private LoggerInterface $logger,
  31. private IProvider $tokenProvider,
  32. ) {
  33. }
  34. /**
  35. * @param string $pollToken
  36. * @return LoginFlowV2Credentials
  37. * @throws LoginFlowV2NotFoundException
  38. */
  39. public function poll(string $pollToken): LoginFlowV2Credentials {
  40. try {
  41. $data = $this->mapper->getByPollToken($this->hashToken($pollToken));
  42. } catch (DoesNotExistException $e) {
  43. throw new LoginFlowV2NotFoundException('Invalid token');
  44. }
  45. $loginName = $data->getLoginName();
  46. $server = $data->getServer();
  47. $appPassword = $data->getAppPassword();
  48. if ($loginName === null || $server === null || $appPassword === null) {
  49. throw new LoginFlowV2NotFoundException('Token not yet ready');
  50. }
  51. // Remove the data from the DB
  52. $this->mapper->delete($data);
  53. try {
  54. // Decrypt the apptoken
  55. $privateKey = $this->crypto->decrypt($data->getPrivateKey(), $pollToken);
  56. $appPassword = $this->decryptPassword($data->getAppPassword(), $privateKey);
  57. } catch (\Exception $e) {
  58. throw new LoginFlowV2NotFoundException('Apptoken could not be decrypted');
  59. }
  60. return new LoginFlowV2Credentials($server, $loginName, $appPassword);
  61. }
  62. /**
  63. * @param string $loginToken
  64. * @return LoginFlowV2
  65. * @throws LoginFlowV2NotFoundException
  66. */
  67. public function getByLoginToken(string $loginToken): LoginFlowV2 {
  68. try {
  69. return $this->mapper->getByLoginToken($loginToken);
  70. } catch (DoesNotExistException $e) {
  71. throw new LoginFlowV2NotFoundException('Login token invalid');
  72. }
  73. }
  74. /**
  75. * @param string $loginToken
  76. * @return bool returns true if the start was successfull. False if not.
  77. */
  78. public function startLoginFlow(string $loginToken): bool {
  79. try {
  80. $data = $this->mapper->getByLoginToken($loginToken);
  81. } catch (DoesNotExistException $e) {
  82. return false;
  83. }
  84. $data->setStarted(1);
  85. $this->mapper->update($data);
  86. return true;
  87. }
  88. /**
  89. * @param string $loginToken
  90. * @param string $sessionId
  91. * @param string $server
  92. * @param string $userId
  93. * @return bool true if the flow was successfully completed false otherwise
  94. */
  95. public function flowDone(string $loginToken, string $sessionId, string $server, string $userId): bool {
  96. try {
  97. $data = $this->mapper->getByLoginToken($loginToken);
  98. } catch (DoesNotExistException $e) {
  99. return false;
  100. }
  101. try {
  102. $sessionToken = $this->tokenProvider->getToken($sessionId);
  103. $loginName = $sessionToken->getLoginName();
  104. try {
  105. $password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
  106. } catch (PasswordlessTokenException $ex) {
  107. $password = null;
  108. }
  109. } catch (InvalidTokenException $ex) {
  110. return false;
  111. }
  112. $appPassword = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
  113. $this->tokenProvider->generateToken(
  114. $appPassword,
  115. $userId,
  116. $loginName,
  117. $password,
  118. $data->getClientName(),
  119. IToken::PERMANENT_TOKEN,
  120. IToken::DO_NOT_REMEMBER
  121. );
  122. $data->setLoginName($loginName);
  123. $data->setServer($server);
  124. // Properly encrypt
  125. $data->setAppPassword($this->encryptPassword($appPassword, $data->getPublicKey()));
  126. $this->mapper->update($data);
  127. return true;
  128. }
  129. public function flowDoneWithAppPassword(string $loginToken, string $server, string $loginName, string $appPassword): bool {
  130. try {
  131. $data = $this->mapper->getByLoginToken($loginToken);
  132. } catch (DoesNotExistException $e) {
  133. return false;
  134. }
  135. $data->setLoginName($loginName);
  136. $data->setServer($server);
  137. // Properly encrypt
  138. $data->setAppPassword($this->encryptPassword($appPassword, $data->getPublicKey()));
  139. $this->mapper->update($data);
  140. return true;
  141. }
  142. public function createTokens(string $userAgent): LoginFlowV2Tokens {
  143. $flow = new LoginFlowV2();
  144. $pollToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER);
  145. $loginToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER);
  146. $flow->setPollToken($this->hashToken($pollToken));
  147. $flow->setLoginToken($loginToken);
  148. $flow->setStarted(0);
  149. $flow->setTimestamp($this->time->getTime());
  150. $flow->setClientName($userAgent);
  151. [$publicKey, $privateKey] = $this->getKeyPair();
  152. $privateKey = $this->crypto->encrypt($privateKey, $pollToken);
  153. $flow->setPublicKey($publicKey);
  154. $flow->setPrivateKey($privateKey);
  155. $this->mapper->insert($flow);
  156. return new LoginFlowV2Tokens($loginToken, $pollToken);
  157. }
  158. private function hashToken(string $token): string {
  159. $secret = $this->config->getSystemValue('secret');
  160. return hash('sha512', $token . $secret);
  161. }
  162. private function getKeyPair(): array {
  163. $config = array_merge([
  164. 'digest_alg' => 'sha512',
  165. 'private_key_bits' => 2048,
  166. ], $this->config->getSystemValue('openssl', []));
  167. // Generate new key
  168. $res = openssl_pkey_new($config);
  169. if ($res === false) {
  170. $this->logOpensslError();
  171. throw new \RuntimeException('Could not initialize keys');
  172. }
  173. if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
  174. $this->logOpensslError();
  175. throw new \RuntimeException('OpenSSL reported a problem');
  176. }
  177. // Extract the public key from $res to $pubKey
  178. $publicKey = openssl_pkey_get_details($res);
  179. $publicKey = $publicKey['key'];
  180. return [$publicKey, $privateKey];
  181. }
  182. private function logOpensslError(): void {
  183. $errors = [];
  184. while ($error = openssl_error_string()) {
  185. $errors[] = $error;
  186. }
  187. $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
  188. }
  189. private function encryptPassword(string $password, string $publicKey): string {
  190. openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
  191. $encryptedPassword = base64_encode($encryptedPassword);
  192. return $encryptedPassword;
  193. }
  194. private function decryptPassword(string $encryptedPassword, string $privateKey): string {
  195. $encryptedPassword = base64_decode($encryptedPassword);
  196. openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
  197. return $password;
  198. }
  199. }