user_service.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. import 'dart:async';
  2. import "dart:convert";
  3. import "dart:math";
  4. import 'package:bip39/bip39.dart' as bip39;
  5. import 'package:dio/dio.dart';
  6. import 'package:ente_auth/core/configuration.dart';
  7. import 'package:ente_auth/core/constants.dart';
  8. import 'package:ente_auth/core/errors.dart';
  9. import 'package:ente_auth/core/event_bus.dart';
  10. import 'package:ente_auth/core/network.dart';
  11. import 'package:ente_auth/events/user_details_changed_event.dart';
  12. import 'package:ente_auth/l10n/l10n.dart';
  13. import 'package:ente_auth/models/api/user/srp.dart';
  14. import 'package:ente_auth/models/delete_account.dart';
  15. import 'package:ente_auth/models/key_attributes.dart';
  16. import 'package:ente_auth/models/key_gen_result.dart';
  17. import 'package:ente_auth/models/sessions.dart';
  18. import 'package:ente_auth/models/set_keys_request.dart';
  19. import 'package:ente_auth/models/set_recovery_key_request.dart';
  20. import 'package:ente_auth/models/user_details.dart';
  21. import 'package:ente_auth/ui/account/login_page.dart';
  22. import 'package:ente_auth/ui/account/ott_verification_page.dart';
  23. import 'package:ente_auth/ui/account/password_entry_page.dart';
  24. import 'package:ente_auth/ui/account/password_reentry_page.dart';
  25. import 'package:ente_auth/ui/account/recovery_page.dart';
  26. import 'package:ente_auth/ui/common/progress_dialog.dart';
  27. import 'package:ente_auth/ui/home_page.dart';
  28. import 'package:ente_auth/ui/two_factor_authentication_page.dart';
  29. import 'package:ente_auth/ui/two_factor_recovery_page.dart';
  30. import 'package:ente_auth/utils/crypto_util.dart';
  31. import 'package:ente_auth/utils/dialog_util.dart';
  32. import 'package:ente_auth/utils/toast_util.dart';
  33. import "package:flutter/foundation.dart";
  34. import 'package:flutter/material.dart';
  35. import 'package:logging/logging.dart';
  36. import "package:pointycastle/export.dart";
  37. import "package:pointycastle/srp/srp6_client.dart";
  38. import "package:pointycastle/srp/srp6_standard_groups.dart";
  39. import "package:pointycastle/srp/srp6_util.dart";
  40. import "package:pointycastle/srp/srp6_verifier_generator.dart";
  41. import 'package:shared_preferences/shared_preferences.dart';
  42. import "package:uuid/uuid.dart";
  43. class UserService {
  44. static const keyHasEnabledTwoFactor = "has_enabled_two_factor";
  45. static const keyUserDetails = "user_details";
  46. static const kReferralSource = "referral_source";
  47. static const kCanDisableEmailMFA = "can_disable_email_mfa";
  48. static const kIsEmailMFAEnabled = "is_email_mfa_enabled";
  49. final SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096;
  50. final _dio = Network.instance.getDio();
  51. final _enteDio = Network.instance.enteDio;
  52. final _logger = Logger((UserService).toString());
  53. final _config = Configuration.instance;
  54. late SharedPreferences _preferences;
  55. late ValueNotifier<String?> emailValueNotifier;
  56. UserService._privateConstructor();
  57. static final UserService instance = UserService._privateConstructor();
  58. Future<void> init() async {
  59. emailValueNotifier =
  60. ValueNotifier<String?>(Configuration.instance.getEmail());
  61. _preferences = await SharedPreferences.getInstance();
  62. }
  63. Future<void> sendOtt(
  64. BuildContext context,
  65. String email, {
  66. bool isChangeEmail = false,
  67. bool isCreateAccountScreen = false,
  68. bool isResetPasswordScreen = false,
  69. }) async {
  70. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  71. await dialog.show();
  72. try {
  73. final response = await _dio.post(
  74. _config.getHttpEndpoint() + "/users/ott",
  75. data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
  76. );
  77. await dialog.hide();
  78. if (response.statusCode == 200) {
  79. unawaited(
  80. Navigator.of(context).push(
  81. MaterialPageRoute(
  82. builder: (BuildContext context) {
  83. return OTTVerificationPage(
  84. email,
  85. isChangeEmail: isChangeEmail,
  86. isCreateAccountScreen: isCreateAccountScreen,
  87. isResetPasswordScreen: isResetPasswordScreen,
  88. );
  89. },
  90. ),
  91. ),
  92. );
  93. return;
  94. }
  95. unawaited(showGenericErrorDialog(context: context));
  96. } on DioError catch (e) {
  97. await dialog.hide();
  98. _logger.info(e);
  99. if (e.response != null && e.response!.statusCode == 403) {
  100. unawaited(
  101. showErrorDialog(
  102. context,
  103. context.l10n.oops,
  104. context.l10n.thisEmailIsAlreadyInUse,
  105. ),
  106. );
  107. } else {
  108. unawaited(showGenericErrorDialog(context: context));
  109. }
  110. } catch (e) {
  111. await dialog.hide();
  112. _logger.severe(e);
  113. unawaited(showGenericErrorDialog(context: context));
  114. }
  115. }
  116. Future<void> sendFeedback(
  117. BuildContext context,
  118. String feedback, {
  119. String type = "SubCancellation",
  120. }) async {
  121. await _dio.post(
  122. _config.getHttpEndpoint() + "/anonymous/feedback",
  123. data: {"feedback": feedback, "type": "type"},
  124. );
  125. }
  126. Future<UserDetails> getUserDetailsV2({
  127. bool memoryCount = false,
  128. bool shouldCache = true,
  129. }) async {
  130. try {
  131. final response = await _enteDio.get(
  132. "/users/details/v2",
  133. queryParameters: {
  134. "memoryCount": memoryCount,
  135. },
  136. );
  137. final userDetails = UserDetails.fromMap(response.data);
  138. if (shouldCache) {
  139. if (userDetails.profileData != null) {
  140. _preferences.setBool(
  141. kIsEmailMFAEnabled,
  142. userDetails.profileData!.isEmailMFAEnabled,
  143. );
  144. _preferences.setBool(
  145. kCanDisableEmailMFA,
  146. userDetails.profileData!.canDisableEmailMFA,
  147. );
  148. }
  149. // handle email change from different client
  150. if (userDetails.email != _config.getEmail()) {
  151. setEmail(userDetails.email);
  152. }
  153. }
  154. return userDetails;
  155. } catch (e) {
  156. _logger.warning("Failed to fetch", e);
  157. rethrow;
  158. }
  159. }
  160. Future<Sessions> getActiveSessions() async {
  161. try {
  162. final response = await _enteDio.get("/users/sessions");
  163. return Sessions.fromMap(response.data);
  164. } on DioError catch (e) {
  165. _logger.info(e);
  166. rethrow;
  167. }
  168. }
  169. Future<void> terminateSession(String token) async {
  170. try {
  171. await _enteDio.delete(
  172. "/users/session",
  173. queryParameters: {
  174. "token": token,
  175. },
  176. );
  177. } on DioError catch (e) {
  178. _logger.info(e);
  179. rethrow;
  180. }
  181. }
  182. Future<void> leaveFamilyPlan() async {
  183. try {
  184. await _enteDio.delete("/family/leave");
  185. } on DioError catch (e) {
  186. _logger.warning('failed to leave family plan', e);
  187. rethrow;
  188. }
  189. }
  190. Future<void> logout(BuildContext context) async {
  191. try {
  192. final response = await _enteDio.post("/users/logout");
  193. if (response.statusCode == 200) {
  194. await Configuration.instance.logout();
  195. Navigator.of(context).popUntil((route) => route.isFirst);
  196. } else {
  197. throw Exception("Log out action failed");
  198. }
  199. } catch (e) {
  200. _logger.severe(e);
  201. //This future is for waiting for the dialog from which logout() is called
  202. //to close and only then to show the error dialog.
  203. Future.delayed(
  204. const Duration(milliseconds: 150),
  205. () => showGenericErrorDialog(context: context),
  206. );
  207. rethrow;
  208. }
  209. }
  210. Future<DeleteChallengeResponse?> getDeleteChallenge(
  211. BuildContext context,
  212. ) async {
  213. try {
  214. final response = await _enteDio.get("/users/delete-challenge");
  215. if (response.statusCode == 200) {
  216. return DeleteChallengeResponse(
  217. allowDelete: response.data["allowDelete"] as bool,
  218. encryptedChallenge: response.data["encryptedChallenge"],
  219. );
  220. } else {
  221. throw Exception("delete action failed");
  222. }
  223. } catch (e) {
  224. _logger.severe(e);
  225. await showGenericErrorDialog(context: context);
  226. return null;
  227. }
  228. }
  229. Future<void> deleteAccount(
  230. BuildContext context,
  231. String challengeResponse,
  232. ) async {
  233. try {
  234. final response = await _enteDio.delete(
  235. "/users/delete",
  236. data: {
  237. "challenge": challengeResponse,
  238. },
  239. );
  240. if (response.statusCode == 200) {
  241. // clear data
  242. await Configuration.instance.logout();
  243. } else {
  244. throw Exception("delete action failed");
  245. }
  246. } catch (e) {
  247. _logger.severe(e);
  248. rethrow;
  249. }
  250. }
  251. Future<void> verifyEmail(
  252. BuildContext context,
  253. String ott, {
  254. bool isResettingPasswordScreen = false,
  255. }) async {
  256. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  257. await dialog.show();
  258. final verifyData = {
  259. "email": _config.getEmail(),
  260. "ott": ott,
  261. };
  262. if (!_config.isLoggedIn()) {
  263. verifyData["source"] = 'auth:' + _getRefSource();
  264. }
  265. try {
  266. final response = await _dio.post(
  267. _config.getHttpEndpoint() + "/users/verify-email",
  268. data: verifyData,
  269. );
  270. await dialog.hide();
  271. if (response.statusCode == 200) {
  272. Widget page;
  273. final String twoFASessionID = response.data["twoFactorSessionID"];
  274. if (twoFASessionID.isNotEmpty) {
  275. page = TwoFactorAuthenticationPage(twoFASessionID);
  276. } else {
  277. await _saveConfiguration(response);
  278. if (Configuration.instance.getEncryptedToken() != null) {
  279. if (isResettingPasswordScreen) {
  280. page = const RecoveryPage();
  281. } else {
  282. page = const PasswordReentryPage();
  283. }
  284. } else {
  285. page = const PasswordEntryPage(
  286. mode: PasswordEntryMode.set,
  287. );
  288. }
  289. }
  290. Navigator.of(context).pushAndRemoveUntil(
  291. MaterialPageRoute(
  292. builder: (BuildContext context) {
  293. return page;
  294. },
  295. ),
  296. (route) => route.isFirst,
  297. );
  298. } else {
  299. // should never reach here
  300. throw Exception("unexpected response during email verification");
  301. }
  302. } on DioError catch (e) {
  303. _logger.info(e);
  304. await dialog.hide();
  305. if (e.response != null && e.response!.statusCode == 410) {
  306. await showErrorDialog(
  307. context,
  308. context.l10n.oops,
  309. context.l10n.yourVerificationCodeHasExpired,
  310. );
  311. Navigator.of(context).pop();
  312. } else {
  313. showErrorDialog(
  314. context,
  315. context.l10n.incorrectCode,
  316. context.l10n.sorryTheCodeYouveEnteredIsIncorrect,
  317. );
  318. }
  319. } catch (e) {
  320. await dialog.hide();
  321. _logger.severe(e);
  322. showErrorDialog(
  323. context,
  324. context.l10n.oops,
  325. context.l10n.verificationFailedPleaseTryAgain,
  326. );
  327. }
  328. }
  329. Future<void> setEmail(String email) async {
  330. await _config.setEmail(email);
  331. emailValueNotifier.value = email;
  332. }
  333. Future<void> changeEmail(
  334. BuildContext context,
  335. String email,
  336. String ott,
  337. ) async {
  338. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  339. await dialog.show();
  340. try {
  341. final response = await _enteDio.post(
  342. "/users/change-email",
  343. data: {
  344. "email": email,
  345. "ott": ott,
  346. },
  347. );
  348. await dialog.hide();
  349. if (response.statusCode == 200) {
  350. showShortToast(context, context.l10n.emailChangedTo(email));
  351. await setEmail(email);
  352. Navigator.of(context).popUntil((route) => route.isFirst);
  353. Bus.instance.fire(UserDetailsChangedEvent());
  354. return;
  355. }
  356. showErrorDialog(
  357. context,
  358. context.l10n.oops,
  359. context.l10n.verificationFailedPleaseTryAgain,
  360. );
  361. } on DioError catch (e) {
  362. await dialog.hide();
  363. if (e.response != null && e.response!.statusCode == 403) {
  364. showErrorDialog(
  365. context,
  366. context.l10n.oops,
  367. context.l10n.thisEmailIsAlreadyInUse,
  368. );
  369. } else {
  370. showErrorDialog(
  371. context,
  372. context.l10n.incorrectCode,
  373. context.l10n.authenticationFailedPleaseTryAgain,
  374. );
  375. }
  376. } catch (e) {
  377. await dialog.hide();
  378. _logger.severe(e);
  379. showErrorDialog(
  380. context,
  381. context.l10n.oops,
  382. context.l10n.verificationFailedPleaseTryAgain,
  383. );
  384. }
  385. }
  386. Future<void> setAttributes(KeyGenResult result) async {
  387. try {
  388. await registerOrUpdateSrp(result.loginKey);
  389. await _enteDio.put(
  390. "/users/attributes",
  391. data: {
  392. "keyAttributes": result.keyAttributes.toMap(),
  393. },
  394. );
  395. await _config.setKey(result.privateKeyAttributes.key);
  396. await _config.setSecretKey(result.privateKeyAttributes.secretKey);
  397. await _config.setKeyAttributes(result.keyAttributes);
  398. } catch (e) {
  399. _logger.severe(e);
  400. rethrow;
  401. }
  402. }
  403. Future<SrpAttributes> getSrpAttributes(String email) async {
  404. try {
  405. final response = await _dio.get(
  406. _config.getHttpEndpoint() + "/users/srp/attributes",
  407. queryParameters: {
  408. "email": email,
  409. },
  410. );
  411. if (response.statusCode == 200) {
  412. return SrpAttributes.fromMap(response.data);
  413. } else {
  414. throw Exception("get-srp-attributes action failed");
  415. }
  416. } on DioError catch (e) {
  417. if (e.response != null && e.response!.statusCode == 404) {
  418. throw SrpSetupNotCompleteError();
  419. }
  420. rethrow;
  421. } catch (e) {
  422. rethrow;
  423. }
  424. }
  425. Future<void> registerOrUpdateSrp(
  426. Uint8List loginKey, {
  427. SetKeysRequest? setKeysRequest,
  428. bool logOutOtherDevices = false,
  429. }) async {
  430. try {
  431. final String username = const Uuid().v4().toString();
  432. final SecureRandom random = _getSecureRandom();
  433. final Uint8List identity = Uint8List.fromList(utf8.encode(username));
  434. final Uint8List password = loginKey;
  435. final Uint8List salt = random.nextBytes(16);
  436. final gen = SRP6VerifierGenerator(
  437. group: kDefaultSrpGroup,
  438. digest: Digest('SHA-256'),
  439. );
  440. final v = gen.generateVerifier(salt, identity, password);
  441. final client = SRP6Client(
  442. group: kDefaultSrpGroup,
  443. digest: Digest('SHA-256'),
  444. random: random,
  445. );
  446. final A = client.generateClientCredentials(salt, identity, password);
  447. final request = SetupSRPRequest(
  448. srpUserID: username,
  449. srpSalt: base64Encode(salt),
  450. srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)),
  451. srpA: base64Encode(SRP6Util.encodeBigInt(A!)),
  452. isUpdate: false,
  453. );
  454. final response = await _enteDio.post(
  455. "/users/srp/setup",
  456. data: request.toMap(),
  457. );
  458. if (response.statusCode == 200) {
  459. final SetupSRPResponse setupSRPResponse =
  460. SetupSRPResponse.fromJson(response.data);
  461. final serverB =
  462. SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
  463. // ignore: need to calculate secret to get M1, unused_local_variable
  464. final clientS = client.calculateSecret(serverB);
  465. final clientM = client.calculateClientEvidenceMessage();
  466. // ignore: unused_local_variable
  467. late Response srpCompleteResponse;
  468. if (setKeysRequest == null) {
  469. srpCompleteResponse = await _enteDio.post(
  470. "/users/srp/complete",
  471. data: {
  472. 'setupID': setupSRPResponse.setupID,
  473. 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
  474. },
  475. );
  476. } else {
  477. srpCompleteResponse = await _enteDio.post(
  478. "/users/srp/update",
  479. data: {
  480. 'setupID': setupSRPResponse.setupID,
  481. 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
  482. 'updatedKeyAttr': setKeysRequest.toMap(),
  483. 'logOutOtherDevices': logOutOtherDevices,
  484. },
  485. );
  486. }
  487. } else {
  488. throw Exception("register-srp action failed");
  489. }
  490. } catch (e, s) {
  491. _logger.severe("failed to register srp", e, s);
  492. rethrow;
  493. }
  494. }
  495. SecureRandom _getSecureRandom() {
  496. final List<int> seeds = [];
  497. final random = Random.secure();
  498. for (int i = 0; i < 32; i++) {
  499. seeds.add(random.nextInt(255));
  500. }
  501. final secureRandom = FortunaRandom();
  502. secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
  503. return secureRandom;
  504. }
  505. Future<void> verifyEmailViaPassword(
  506. BuildContext context,
  507. SrpAttributes srpAttributes,
  508. String userPassword,
  509. ProgressDialog dialog,
  510. ) async {
  511. late Uint8List keyEncryptionKey;
  512. _logger.finest('Start deriving key');
  513. keyEncryptionKey = await CryptoUtil.deriveKey(
  514. utf8.encode(userPassword) as Uint8List,
  515. CryptoUtil.base642bin(srpAttributes.kekSalt),
  516. srpAttributes.memLimit,
  517. srpAttributes.opsLimit,
  518. );
  519. _logger.finest('keyDerivation done, derive LoginKey');
  520. final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
  521. final Uint8List identity = Uint8List.fromList(
  522. utf8.encode(srpAttributes.srpUserID),
  523. );
  524. _logger.finest('longinKey derivation done');
  525. final Uint8List salt = base64Decode(srpAttributes.srpSalt);
  526. final Uint8List password = loginKey;
  527. final SecureRandom random = _getSecureRandom();
  528. final client = SRP6Client(
  529. group: kDefaultSrpGroup,
  530. digest: Digest('SHA-256'),
  531. random: random,
  532. );
  533. final A = client.generateClientCredentials(salt, identity, password);
  534. final createSessionResponse = await _dio.post(
  535. _config.getHttpEndpoint() + "/users/srp/create-session",
  536. data: {
  537. "srpUserID": srpAttributes.srpUserID,
  538. "srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
  539. },
  540. );
  541. final String sessionID = createSessionResponse.data["sessionID"];
  542. final String srpB = createSessionResponse.data["srpB"];
  543. final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
  544. // ignore: need to calculate secret to get M1, unused_local_variable
  545. final clientS = client.calculateSecret(serverB);
  546. final clientM = client.calculateClientEvidenceMessage();
  547. final response = await _dio.post(
  548. _config.getHttpEndpoint() + "/users/srp/verify-session",
  549. data: {
  550. "sessionID": sessionID,
  551. "srpUserID": srpAttributes.srpUserID,
  552. "srpM1": base64Encode(SRP6Util.encodeBigInt(clientM!)),
  553. },
  554. );
  555. if (response.statusCode == 200) {
  556. Widget page;
  557. final String twoFASessionID = response.data["twoFactorSessionID"];
  558. Configuration.instance.setVolatilePassword(userPassword);
  559. if (twoFASessionID.isNotEmpty) {
  560. page = TwoFactorAuthenticationPage(twoFASessionID);
  561. } else {
  562. await _saveConfiguration(response);
  563. if (Configuration.instance.getEncryptedToken() != null) {
  564. await Configuration.instance.decryptSecretsAndGetKeyEncKey(
  565. userPassword,
  566. Configuration.instance.getKeyAttributes()!,
  567. keyEncryptionKey: keyEncryptionKey,
  568. );
  569. page = const HomePage();
  570. } else {
  571. throw Exception("unexpected response during email verification");
  572. }
  573. }
  574. await dialog.hide();
  575. Navigator.of(context).pushAndRemoveUntil(
  576. MaterialPageRoute(
  577. builder: (BuildContext context) {
  578. return page;
  579. },
  580. ),
  581. (route) => route.isFirst,
  582. );
  583. } else {
  584. // should never reach here
  585. throw Exception("unexpected response during email verification");
  586. }
  587. }
  588. Future<void> updateKeyAttributes(
  589. KeyAttributes keyAttributes,
  590. Uint8List loginKey, {
  591. required bool logoutOtherDevices,
  592. }) async {
  593. try {
  594. final setKeyRequest = SetKeysRequest(
  595. kekSalt: keyAttributes.kekSalt,
  596. encryptedKey: keyAttributes.encryptedKey,
  597. keyDecryptionNonce: keyAttributes.keyDecryptionNonce,
  598. memLimit: keyAttributes.memLimit,
  599. opsLimit: keyAttributes.opsLimit,
  600. );
  601. await registerOrUpdateSrp(
  602. loginKey,
  603. setKeysRequest: setKeyRequest,
  604. logOutOtherDevices: logoutOtherDevices,
  605. );
  606. await _config.setKeyAttributes(keyAttributes);
  607. } catch (e) {
  608. _logger.severe(e);
  609. rethrow;
  610. }
  611. }
  612. Future<void> setRecoveryKey(KeyAttributes keyAttributes) async {
  613. try {
  614. final setRecoveryKeyRequest = SetRecoveryKeyRequest(
  615. keyAttributes.masterKeyEncryptedWithRecoveryKey,
  616. keyAttributes.masterKeyDecryptionNonce,
  617. keyAttributes.recoveryKeyEncryptedWithMasterKey,
  618. keyAttributes.recoveryKeyDecryptionNonce,
  619. );
  620. await _enteDio.put(
  621. "/users/recovery-key",
  622. data: setRecoveryKeyRequest.toMap(),
  623. );
  624. await _config.setKeyAttributes(keyAttributes);
  625. } catch (e) {
  626. _logger.severe(e);
  627. rethrow;
  628. }
  629. }
  630. Future<void> verifyTwoFactor(
  631. BuildContext context,
  632. String sessionID,
  633. String code,
  634. ) async {
  635. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  636. await dialog.show();
  637. try {
  638. final response = await _dio.post(
  639. _config.getHttpEndpoint() + "/users/two-factor/verify",
  640. data: {
  641. "sessionID": sessionID,
  642. "code": code,
  643. },
  644. );
  645. await dialog.hide();
  646. if (response.statusCode == 200) {
  647. showShortToast(context, context.l10n.authenticationSuccessful);
  648. await _saveConfiguration(response);
  649. Navigator.of(context).pushAndRemoveUntil(
  650. MaterialPageRoute(
  651. builder: (BuildContext context) {
  652. return const PasswordReentryPage();
  653. },
  654. ),
  655. (route) => route.isFirst,
  656. );
  657. }
  658. } on DioError catch (e) {
  659. await dialog.hide();
  660. _logger.severe(e);
  661. if (e.response != null && e.response!.statusCode == 404) {
  662. showToast(context, "Session expired");
  663. Navigator.of(context).pushAndRemoveUntil(
  664. MaterialPageRoute(
  665. builder: (BuildContext context) {
  666. return const LoginPage();
  667. },
  668. ),
  669. (route) => route.isFirst,
  670. );
  671. } else {
  672. showErrorDialog(
  673. context,
  674. context.l10n.incorrectCode,
  675. context.l10n.authenticationFailedPleaseTryAgain,
  676. );
  677. }
  678. } catch (e) {
  679. await dialog.hide();
  680. _logger.severe(e);
  681. showErrorDialog(
  682. context,
  683. context.l10n.oops,
  684. context.l10n.authenticationFailedPleaseTryAgain,
  685. );
  686. }
  687. }
  688. Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
  689. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  690. await dialog.show();
  691. try {
  692. final response = await _dio.get(
  693. _config.getHttpEndpoint() + "/users/two-factor/recover",
  694. queryParameters: {
  695. "sessionID": sessionID,
  696. },
  697. );
  698. if (response.statusCode == 200) {
  699. Navigator.of(context).pushAndRemoveUntil(
  700. MaterialPageRoute(
  701. builder: (BuildContext context) {
  702. return TwoFactorRecoveryPage(
  703. sessionID,
  704. response.data["encryptedSecret"],
  705. response.data["secretDecryptionNonce"],
  706. );
  707. },
  708. ),
  709. (route) => route.isFirst,
  710. );
  711. }
  712. } on DioError catch (e) {
  713. _logger.severe(e);
  714. if (e.response != null && e.response!.statusCode == 404) {
  715. showToast(context, context.l10n.sessionExpired);
  716. Navigator.of(context).pushAndRemoveUntil(
  717. MaterialPageRoute(
  718. builder: (BuildContext context) {
  719. return const LoginPage();
  720. },
  721. ),
  722. (route) => route.isFirst,
  723. );
  724. } else {
  725. showErrorDialog(
  726. context,
  727. context.l10n.oops,
  728. context.l10n.somethingWentWrongPleaseTryAgain,
  729. );
  730. }
  731. } catch (e) {
  732. _logger.severe(e);
  733. showErrorDialog(
  734. context,
  735. context.l10n.oops,
  736. context.l10n.somethingWentWrongPleaseTryAgain,
  737. );
  738. } finally {
  739. await dialog.hide();
  740. }
  741. }
  742. Future<void> removeTwoFactor(
  743. BuildContext context,
  744. String sessionID,
  745. String recoveryKey,
  746. String encryptedSecret,
  747. String secretDecryptionNonce,
  748. ) async {
  749. final dialog = createProgressDialog(context, context.l10n.pleaseWait);
  750. await dialog.show();
  751. String secret;
  752. try {
  753. if (recoveryKey.contains(' ')) {
  754. if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
  755. throw AssertionError(
  756. 'recovery code should have $mnemonicKeyWordCount words',
  757. );
  758. }
  759. recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
  760. }
  761. secret = CryptoUtil.bin2base64(
  762. await CryptoUtil.decrypt(
  763. CryptoUtil.base642bin(encryptedSecret),
  764. CryptoUtil.hex2bin(recoveryKey.trim()),
  765. CryptoUtil.base642bin(secretDecryptionNonce),
  766. ),
  767. );
  768. } catch (e) {
  769. await dialog.hide();
  770. await showErrorDialog(
  771. context,
  772. context.l10n.incorrectRecoveryKey,
  773. context.l10n.theRecoveryKeyYouEnteredIsIncorrect,
  774. );
  775. return;
  776. }
  777. try {
  778. final response = await _dio.post(
  779. _config.getHttpEndpoint() + "/users/two-factor/remove",
  780. data: {
  781. "sessionID": sessionID,
  782. "secret": secret,
  783. },
  784. );
  785. if (response.statusCode == 200) {
  786. showShortToast(
  787. context,
  788. context.l10n.twofactorAuthenticationSuccessfullyReset,
  789. );
  790. await _saveConfiguration(response);
  791. Navigator.of(context).pushAndRemoveUntil(
  792. MaterialPageRoute(
  793. builder: (BuildContext context) {
  794. return const PasswordReentryPage();
  795. },
  796. ),
  797. (route) => route.isFirst,
  798. );
  799. }
  800. } on DioError catch (e) {
  801. _logger.severe(e);
  802. if (e.response != null && e.response!.statusCode == 404) {
  803. showToast(context, "Session expired");
  804. Navigator.of(context).pushAndRemoveUntil(
  805. MaterialPageRoute(
  806. builder: (BuildContext context) {
  807. return const LoginPage();
  808. },
  809. ),
  810. (route) => route.isFirst,
  811. );
  812. } else {
  813. showErrorDialog(
  814. context,
  815. context.l10n.oops,
  816. context.l10n.somethingWentWrongPleaseTryAgain,
  817. );
  818. }
  819. } catch (e) {
  820. _logger.severe(e);
  821. showErrorDialog(
  822. context,
  823. context.l10n.oops,
  824. context.l10n.somethingWentWrongPleaseTryAgain,
  825. );
  826. } finally {
  827. await dialog.hide();
  828. }
  829. }
  830. Future<void> _saveConfiguration(Response response) async {
  831. await Configuration.instance.setUserID(response.data["id"]);
  832. if (response.data["encryptedToken"] != null) {
  833. await Configuration.instance
  834. .setEncryptedToken(response.data["encryptedToken"]);
  835. await Configuration.instance.setKeyAttributes(
  836. KeyAttributes.fromMap(response.data["keyAttributes"]),
  837. );
  838. } else {
  839. await Configuration.instance.setToken(response.data["token"]);
  840. }
  841. }
  842. bool? canDisableEmailMFA() {
  843. return _preferences.getBool(kCanDisableEmailMFA);
  844. }
  845. bool hasEmailMFAEnabled() {
  846. return _preferences.getBool(kIsEmailMFAEnabled) ?? true;
  847. }
  848. Future<void> updateEmailMFA(bool isEnabled) async {
  849. try {
  850. await _enteDio.put(
  851. "/users/email-mfa",
  852. data: {
  853. "isEnabled": isEnabled,
  854. },
  855. );
  856. _preferences.setBool(kIsEmailMFAEnabled, isEnabled);
  857. } catch (e) {
  858. _logger.severe("Failed to update email mfa", e);
  859. rethrow;
  860. }
  861. }
  862. Future<void> setRefSource(String refSource) async {
  863. await _preferences.setString(kReferralSource, refSource);
  864. }
  865. String _getRefSource() {
  866. return _preferences.getString(kReferralSource) ?? "";
  867. }
  868. }