user_service.dart 28 KB

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