mapper->getByPollToken($this->hashToken($pollToken)); } catch (DoesNotExistException $e) { throw new LoginFlowV2NotFoundException('Invalid token'); } $loginName = $data->getLoginName(); $server = $data->getServer(); $appPassword = $data->getAppPassword(); if ($loginName === null || $server === null || $appPassword === null) { throw new LoginFlowV2NotFoundException('Token not yet ready'); } // Remove the data from the DB $this->mapper->delete($data); try { // Decrypt the apptoken $privateKey = $this->crypto->decrypt($data->getPrivateKey(), $pollToken); $appPassword = $this->decryptPassword($data->getAppPassword(), $privateKey); } catch (\Exception $e) { throw new LoginFlowV2NotFoundException('Apptoken could not be decrypted'); } return new LoginFlowV2Credentials($server, $loginName, $appPassword); } /** * @param string $loginToken * @return LoginFlowV2 * @throws LoginFlowV2NotFoundException */ public function getByLoginToken(string $loginToken): LoginFlowV2 { try { return $this->mapper->getByLoginToken($loginToken); } catch (DoesNotExistException $e) { throw new LoginFlowV2NotFoundException('Login token invalid'); } } /** * @param string $loginToken * @return bool returns true if the start was successfull. False if not. */ public function startLoginFlow(string $loginToken): bool { try { $data = $this->mapper->getByLoginToken($loginToken); } catch (DoesNotExistException $e) { return false; } $data->setStarted(1); $this->mapper->update($data); return true; } /** * @param string $loginToken * @param string $sessionId * @param string $server * @param string $userId * @return bool true if the flow was successfully completed false otherwise */ public function flowDone(string $loginToken, string $sessionId, string $server, string $userId): bool { try { $data = $this->mapper->getByLoginToken($loginToken); } catch (DoesNotExistException $e) { return false; } try { $sessionToken = $this->tokenProvider->getToken($sessionId); $loginName = $sessionToken->getLoginName(); try { $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); } catch (PasswordlessTokenException $ex) { $password = null; } } catch (InvalidTokenException $ex) { return false; } $appPassword = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $this->tokenProvider->generateToken( $appPassword, $userId, $loginName, $password, $data->getClientName(), IToken::PERMANENT_TOKEN, IToken::DO_NOT_REMEMBER ); $data->setLoginName($loginName); $data->setServer($server); // Properly encrypt $data->setAppPassword($this->encryptPassword($appPassword, $data->getPublicKey())); $this->mapper->update($data); return true; } public function flowDoneWithAppPassword(string $loginToken, string $server, string $loginName, string $appPassword): bool { try { $data = $this->mapper->getByLoginToken($loginToken); } catch (DoesNotExistException $e) { return false; } $data->setLoginName($loginName); $data->setServer($server); // Properly encrypt $data->setAppPassword($this->encryptPassword($appPassword, $data->getPublicKey())); $this->mapper->update($data); return true; } public function createTokens(string $userAgent): LoginFlowV2Tokens { $flow = new LoginFlowV2(); $pollToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER); $loginToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER); $flow->setPollToken($this->hashToken($pollToken)); $flow->setLoginToken($loginToken); $flow->setStarted(0); $flow->setTimestamp($this->time->getTime()); $flow->setClientName($userAgent); [$publicKey, $privateKey] = $this->getKeyPair(); $privateKey = $this->crypto->encrypt($privateKey, $pollToken); $flow->setPublicKey($publicKey); $flow->setPrivateKey($privateKey); $this->mapper->insert($flow); return new LoginFlowV2Tokens($loginToken, $pollToken); } private function hashToken(string $token): string { $secret = $this->config->getSystemValue('secret'); return hash('sha512', $token . $secret); } private function getKeyPair(): array { $config = array_merge([ 'digest_alg' => 'sha512', 'private_key_bits' => 2048, ], $this->config->getSystemValue('openssl', [])); // Generate new key $res = openssl_pkey_new($config); if ($res === false) { $this->logOpensslError(); throw new \RuntimeException('Could not initialize keys'); } if (openssl_pkey_export($res, $privateKey, null, $config) === false) { $this->logOpensslError(); throw new \RuntimeException('OpenSSL reported a problem'); } // Extract the public key from $res to $pubKey $publicKey = openssl_pkey_get_details($res); $publicKey = $publicKey['key']; return [$publicKey, $privateKey]; } private function logOpensslError(): void { $errors = []; while ($error = openssl_error_string()) { $errors[] = $error; } $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors)); } private function encryptPassword(string $password, string $publicKey): string { openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); $encryptedPassword = base64_encode($encryptedPassword); return $encryptedPassword; } private function decryptPassword(string $encryptedPassword, string $privateKey): string { $encryptedPassword = base64_decode($encryptedPassword); openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); return $password; } }