user_service.dart 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  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:flutter/foundation.dart";
  7. import 'package:flutter/material.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/core/constants.dart';
  11. import "package:photos/core/errors.dart";
  12. import 'package:photos/core/event_bus.dart';
  13. import 'package:photos/core/network/network.dart';
  14. import 'package:photos/db/public_keys_db.dart';
  15. import "package:photos/events/account_configured_event.dart";
  16. import 'package:photos/events/two_factor_status_change_event.dart';
  17. import 'package:photos/events/user_details_changed_event.dart';
  18. import "package:photos/generated/l10n.dart";
  19. import "package:photos/models/api/user/srp.dart";
  20. import 'package:photos/models/delete_account.dart';
  21. import 'package:photos/models/key_attributes.dart';
  22. import 'package:photos/models/key_gen_result.dart';
  23. import 'package:photos/models/public_key.dart' as ePublicKey;
  24. import 'package:photos/models/sessions.dart';
  25. import 'package:photos/models/set_keys_request.dart';
  26. import 'package:photos/models/set_recovery_key_request.dart';
  27. import 'package:photos/models/user_details.dart';
  28. import 'package:photos/ui/account/login_page.dart';
  29. import 'package:photos/ui/account/ott_verification_page.dart';
  30. import 'package:photos/ui/account/password_entry_page.dart';
  31. import 'package:photos/ui/account/password_reentry_page.dart';
  32. import "package:photos/ui/account/recovery_page.dart";
  33. import 'package:photos/ui/account/two_factor_authentication_page.dart';
  34. import 'package:photos/ui/account/two_factor_recovery_page.dart';
  35. import 'package:photos/ui/account/two_factor_setup_page.dart';
  36. import "package:photos/ui/common/progress_dialog.dart";
  37. import "package:photos/ui/tabs/home_widget.dart";
  38. import 'package:photos/utils/crypto_util.dart';
  39. import 'package:photos/utils/dialog_util.dart';
  40. import 'package:photos/utils/navigation_util.dart';
  41. import 'package:photos/utils/toast_util.dart';
  42. import "package:pointycastle/export.dart";
  43. import "package:pointycastle/srp/srp6_client.dart";
  44. import "package:pointycastle/srp/srp6_standard_groups.dart";
  45. import "package:pointycastle/srp/srp6_util.dart";
  46. import "package:pointycastle/srp/srp6_verifier_generator.dart";
  47. import 'package:shared_preferences/shared_preferences.dart';
  48. import "package:uuid/uuid.dart";
  49. class UserService {
  50. static const keyHasEnabledTwoFactor = "has_enabled_two_factor";
  51. static const keyUserDetails = "user_details";
  52. static const kReferralSource = "referral_source";
  53. final SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096;
  54. final _dio = NetworkClient.instance.getDio();
  55. final _enteDio = NetworkClient.instance.enteDio;
  56. final _logger = Logger((UserService).toString());
  57. final _config = Configuration.instance;
  58. late SharedPreferences _preferences;
  59. late ValueNotifier<String?> emailValueNotifier;
  60. UserService._privateConstructor();
  61. static final UserService instance = UserService._privateConstructor();
  62. Future<void> init() async {
  63. emailValueNotifier =
  64. ValueNotifier<String?>(Configuration.instance.getEmail());
  65. _preferences = await SharedPreferences.getInstance();
  66. if (Configuration.instance.isLoggedIn()) {
  67. // add artificial delay in refreshing 2FA status
  68. Future.delayed(
  69. const Duration(seconds: 5),
  70. () => {setTwoFactor(fetchTwoFactorStatus: true).ignore()},
  71. );
  72. }
  73. Bus.instance.on<TwoFactorStatusChangeEvent>().listen((event) {
  74. setTwoFactor(value: event.status);
  75. });
  76. }
  77. Future<void> sendOtt(
  78. BuildContext context,
  79. String email, {
  80. bool isChangeEmail = false,
  81. bool isCreateAccountScreen = false,
  82. bool isResetPasswordScreen = false,
  83. }) async {
  84. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  85. await dialog.show();
  86. try {
  87. final response = await _dio.post(
  88. _config.getHttpEndpoint() + "/users/ott",
  89. data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
  90. );
  91. await dialog.hide();
  92. if (response.statusCode == 200) {
  93. unawaited(
  94. Navigator.of(context).push(
  95. MaterialPageRoute(
  96. builder: (BuildContext context) {
  97. return OTTVerificationPage(
  98. email,
  99. isChangeEmail: isChangeEmail,
  100. isCreateAccountScreen: isCreateAccountScreen,
  101. isResetPasswordScreen: isResetPasswordScreen,
  102. );
  103. },
  104. ),
  105. ),
  106. );
  107. return;
  108. }
  109. unawaited(showGenericErrorDialog(context: context));
  110. } on DioError catch (e) {
  111. await dialog.hide();
  112. _logger.info(e);
  113. if (e.response != null && e.response!.statusCode == 403) {
  114. unawaited(
  115. showErrorDialog(
  116. context,
  117. S.of(context).oops,
  118. S.of(context).thisEmailIsAlreadyInUse,
  119. ),
  120. );
  121. } else {
  122. unawaited(showGenericErrorDialog(context: context));
  123. }
  124. } catch (e) {
  125. await dialog.hide();
  126. _logger.severe(e);
  127. unawaited(showGenericErrorDialog(context: context));
  128. }
  129. }
  130. Future<void> sendFeedback(
  131. BuildContext context,
  132. String feedback, {
  133. String type = "SubCancellation",
  134. }) async {
  135. await _dio.post(
  136. _config.getHttpEndpoint() + "/anonymous/feedback",
  137. data: {"feedback": feedback, "type": "type"},
  138. );
  139. }
  140. // getPublicKey returns null value if email id is not
  141. // associated with another ente account
  142. Future<String?> getPublicKey(String email) async {
  143. try {
  144. final response = await _enteDio.get(
  145. "/users/public-key",
  146. queryParameters: {"email": email},
  147. );
  148. final publicKey = response.data["publicKey"];
  149. await PublicKeysDB.instance.setKey(
  150. ePublicKey.PublicKey(
  151. email,
  152. publicKey,
  153. ),
  154. );
  155. return publicKey;
  156. } on DioError catch (e) {
  157. if (e.response != null && e.response?.statusCode == 404) {
  158. return null;
  159. }
  160. rethrow;
  161. }
  162. }
  163. UserDetails? getCachedUserDetails() {
  164. if (_preferences.containsKey(keyUserDetails)) {
  165. return UserDetails.fromJson(_preferences.getString(keyUserDetails)!);
  166. }
  167. return null;
  168. }
  169. Future<UserDetails> getUserDetailsV2({
  170. bool memoryCount = true,
  171. bool shouldCache = false,
  172. }) async {
  173. _logger.info("Fetching user details");
  174. try {
  175. final response = await _enteDio.get(
  176. "/users/details/v2",
  177. queryParameters: {
  178. "memoryCount": memoryCount,
  179. },
  180. );
  181. final userDetails = UserDetails.fromMap(response.data);
  182. if (shouldCache) {
  183. await _preferences.setString(keyUserDetails, userDetails.toJson());
  184. // handle email change from different client
  185. if (userDetails.email != _config.getEmail()) {
  186. setEmail(userDetails.email);
  187. }
  188. }
  189. return userDetails;
  190. } on DioError catch (e) {
  191. _logger.info(e);
  192. rethrow;
  193. }
  194. }
  195. Future<Sessions> getActiveSessions() async {
  196. try {
  197. final response = await _enteDio.get("/users/sessions");
  198. return Sessions.fromMap(response.data);
  199. } on DioError catch (e) {
  200. _logger.info(e);
  201. rethrow;
  202. }
  203. }
  204. Future<void> terminateSession(String token) async {
  205. try {
  206. await _enteDio.delete(
  207. "/users/session",
  208. queryParameters: {
  209. "token": token,
  210. },
  211. );
  212. } on DioError catch (e) {
  213. _logger.info(e);
  214. rethrow;
  215. }
  216. }
  217. Future<void> leaveFamilyPlan() async {
  218. try {
  219. await _enteDio.delete("/family/leave");
  220. } on DioError catch (e) {
  221. _logger.warning('failed to leave family plan', e);
  222. rethrow;
  223. }
  224. }
  225. Future<void> logout(BuildContext context) async {
  226. try {
  227. final response = await _enteDio.post("/users/logout");
  228. if (response.statusCode == 200) {
  229. await Configuration.instance.logout();
  230. Navigator.of(context).popUntil((route) => route.isFirst);
  231. } else {
  232. throw Exception("Log out action failed");
  233. }
  234. } catch (e) {
  235. // check if token is already invalid
  236. if (e is DioError && e.response?.statusCode == 401) {
  237. await Configuration.instance.logout();
  238. Navigator.of(context).popUntil((route) => route.isFirst);
  239. return;
  240. }
  241. _logger.severe("Failed to logout", e);
  242. //This future is for waiting for the dialog from which logout() is called
  243. //to close and only then to show the error dialog.
  244. Future.delayed(
  245. const Duration(milliseconds: 150),
  246. () => showGenericErrorDialog(context: context),
  247. );
  248. }
  249. }
  250. Future<DeleteChallengeResponse?> getDeleteChallenge(
  251. BuildContext context,
  252. ) async {
  253. try {
  254. final response = await _enteDio.get("/users/delete-challenge");
  255. if (response.statusCode == 200) {
  256. return DeleteChallengeResponse(
  257. allowDelete: response.data["allowDelete"] as bool,
  258. encryptedChallenge: response.data["encryptedChallenge"],
  259. );
  260. } else {
  261. throw Exception("delete action failed");
  262. }
  263. } catch (e) {
  264. _logger.severe(e);
  265. await showGenericErrorDialog(context: context);
  266. return null;
  267. }
  268. }
  269. Future<void> deleteAccount(
  270. BuildContext context,
  271. String challengeResponse, {
  272. required String reasonCategory,
  273. required String feedback,
  274. }) async {
  275. try {
  276. final response = await _enteDio.delete(
  277. "/users/delete",
  278. data: {
  279. "challenge": challengeResponse,
  280. "reasonCategory": reasonCategory,
  281. "feedback": feedback,
  282. },
  283. );
  284. if (response.statusCode == 200) {
  285. // clear data
  286. await Configuration.instance.logout();
  287. } else {
  288. throw Exception("delete action failed");
  289. }
  290. } catch (e) {
  291. _logger.severe(e);
  292. rethrow;
  293. }
  294. }
  295. Future<void> verifyEmail(
  296. BuildContext context,
  297. String ott, {
  298. bool isResettingPasswordScreen = false,
  299. }) async {
  300. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  301. await dialog.show();
  302. final verifyData = {
  303. "email": _config.getEmail(),
  304. "ott": ott,
  305. };
  306. if (!_config.isLoggedIn()) {
  307. verifyData["source"] = _getRefSource();
  308. }
  309. try {
  310. final response = await _dio.post(
  311. _config.getHttpEndpoint() + "/users/verify-email",
  312. data: verifyData,
  313. );
  314. await dialog.hide();
  315. if (response.statusCode == 200) {
  316. Widget page;
  317. final String twoFASessionID = response.data["twoFactorSessionID"];
  318. if (twoFASessionID.isNotEmpty) {
  319. setTwoFactor(value: true);
  320. page = TwoFactorAuthenticationPage(twoFASessionID);
  321. } else {
  322. await _saveConfiguration(response);
  323. if (Configuration.instance.getEncryptedToken() != null) {
  324. if (isResettingPasswordScreen) {
  325. page = const RecoveryPage();
  326. } else {
  327. page = const PasswordReentryPage();
  328. }
  329. } else {
  330. page = const PasswordEntryPage(
  331. mode: PasswordEntryMode.set,
  332. );
  333. }
  334. }
  335. Navigator.of(context).pushAndRemoveUntil(
  336. MaterialPageRoute(
  337. builder: (BuildContext context) {
  338. return page;
  339. },
  340. ),
  341. (route) => route.isFirst,
  342. );
  343. } else {
  344. // should never reach here
  345. throw Exception("unexpected response during email verification");
  346. }
  347. } on DioError catch (e) {
  348. _logger.info(e);
  349. await dialog.hide();
  350. if (e.response != null && e.response!.statusCode == 410) {
  351. await showErrorDialog(
  352. context,
  353. S.of(context).oops,
  354. S.of(context).yourVerificationCodeHasExpired,
  355. );
  356. Navigator.of(context).pop();
  357. } else {
  358. showErrorDialog(
  359. context,
  360. S.of(context).incorrectCode,
  361. S.of(context).sorryTheCodeYouveEnteredIsIncorrect,
  362. );
  363. }
  364. } catch (e) {
  365. await dialog.hide();
  366. _logger.severe(e);
  367. showErrorDialog(
  368. context,
  369. S.of(context).oops,
  370. S.of(context).verificationFailedPleaseTryAgain,
  371. );
  372. }
  373. }
  374. Future<void> setEmail(String email) async {
  375. await _config.setEmail(email);
  376. emailValueNotifier.value = email;
  377. }
  378. Future<void> setRefSource(String refSource) async {
  379. await _preferences.setString(kReferralSource, refSource);
  380. }
  381. String _getRefSource() {
  382. return _preferences.getString(kReferralSource) ?? "";
  383. }
  384. Future<void> changeEmail(
  385. BuildContext context,
  386. String email,
  387. String ott,
  388. ) async {
  389. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  390. await dialog.show();
  391. try {
  392. final response = await _enteDio.post(
  393. "/users/change-email",
  394. data: {
  395. "email": email,
  396. "ott": ott,
  397. },
  398. );
  399. await dialog.hide();
  400. if (response.statusCode == 200) {
  401. showShortToast(context, S.of(context).emailChangedTo(email));
  402. await setEmail(email);
  403. Navigator.of(context).popUntil((route) => route.isFirst);
  404. Bus.instance.fire(UserDetailsChangedEvent());
  405. return;
  406. }
  407. showErrorDialog(
  408. context,
  409. S.of(context).oops,
  410. S.of(context).verificationFailedPleaseTryAgain,
  411. );
  412. } on DioError catch (e) {
  413. await dialog.hide();
  414. if (e.response != null && e.response!.statusCode == 403) {
  415. showErrorDialog(
  416. context,
  417. S.of(context).oops,
  418. S.of(context).thisEmailIsAlreadyInUse,
  419. );
  420. } else {
  421. showErrorDialog(
  422. context,
  423. S.of(context).incorrectCode,
  424. S.of(context).authenticationFailedPleaseTryAgain,
  425. );
  426. }
  427. } catch (e) {
  428. await dialog.hide();
  429. _logger.severe(e);
  430. showErrorDialog(
  431. context,
  432. S.of(context).oops,
  433. S.of(context).verificationFailedPleaseTryAgain,
  434. );
  435. }
  436. }
  437. Future<void> setAttributes(KeyGenResult result) async {
  438. try {
  439. await registerOrUpdateSrp(result.loginKey);
  440. await _enteDio.put(
  441. "/users/attributes",
  442. data: {
  443. "keyAttributes": result.keyAttributes.toMap(),
  444. },
  445. );
  446. await _config.setKey(result.privateKeyAttributes.key);
  447. await _config.setSecretKey(result.privateKeyAttributes.secretKey);
  448. await _config.setKeyAttributes(result.keyAttributes);
  449. } catch (e) {
  450. _logger.severe(e);
  451. rethrow;
  452. }
  453. }
  454. Future<SrpAttributes> getSrpAttributes(String email) async {
  455. try {
  456. final response = await _dio.get(
  457. _config.getHttpEndpoint() + "/users/srp/attributes",
  458. queryParameters: {
  459. "email": email,
  460. },
  461. );
  462. if (response.statusCode == 200) {
  463. return SrpAttributes.fromMap(response.data);
  464. } else {
  465. throw Exception("get-srp-attributes action failed");
  466. }
  467. } on DioError catch (e) {
  468. if (e.response != null && e.response!.statusCode == 404) {
  469. throw SrpSetupNotCompleteError();
  470. }
  471. rethrow;
  472. } catch (e) {
  473. rethrow;
  474. }
  475. }
  476. Future<void> registerOrUpdateSrp(
  477. Uint8List loginKey, {
  478. SetKeysRequest? setKeysRequest,
  479. }) async {
  480. try {
  481. final String username = const Uuid().v4().toString();
  482. final SecureRandom random = _getSecureRandom();
  483. final Uint8List identity = Uint8List.fromList(utf8.encode(username));
  484. final Uint8List password = loginKey;
  485. final Uint8List salt = random.nextBytes(16);
  486. final gen = SRP6VerifierGenerator(
  487. group: kDefaultSrpGroup,
  488. digest: Digest('SHA-256'),
  489. );
  490. final v = gen.generateVerifier(salt, identity, password);
  491. final client = SRP6Client(
  492. group: kDefaultSrpGroup,
  493. digest: Digest('SHA-256'),
  494. random: random,
  495. );
  496. final A = client.generateClientCredentials(salt, identity, password);
  497. final request = SetupSRPRequest(
  498. srpUserID: username,
  499. srpSalt: base64Encode(salt),
  500. srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)),
  501. srpA: base64Encode(SRP6Util.encodeBigInt(A!)),
  502. isUpdate: false,
  503. );
  504. final response = await _enteDio.post(
  505. "/users/srp/setup",
  506. data: request.toMap(),
  507. );
  508. if (response.statusCode == 200) {
  509. final SetupSRPResponse setupSRPResponse =
  510. SetupSRPResponse.fromJson(response.data);
  511. final serverB =
  512. SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
  513. // ignore: need to calculate secret to get M1, unused_local_variable
  514. final clientS = client.calculateSecret(serverB);
  515. final clientM = client.calculateClientEvidenceMessage();
  516. // ignore: unused_local_variable
  517. late Response srpCompleteResponse;
  518. if (setKeysRequest == null) {
  519. srpCompleteResponse = await _enteDio.post(
  520. "/users/srp/complete",
  521. data: {
  522. 'setupID': setupSRPResponse.setupID,
  523. 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
  524. },
  525. );
  526. } else {
  527. srpCompleteResponse = await _enteDio.post(
  528. "/users/srp/update",
  529. data: {
  530. 'setupID': setupSRPResponse.setupID,
  531. 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
  532. 'updatedKeyAttr': setKeysRequest.toMap(),
  533. },
  534. );
  535. }
  536. } else {
  537. throw Exception("register-srp action failed");
  538. }
  539. } catch (e, s) {
  540. _logger.severe("failed to register srp", e, s);
  541. rethrow;
  542. }
  543. }
  544. SecureRandom _getSecureRandom() {
  545. final List<int> seeds = [];
  546. final random = Random.secure();
  547. for (int i = 0; i < 32; i++) {
  548. seeds.add(random.nextInt(255));
  549. }
  550. final secureRandom = FortunaRandom();
  551. secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
  552. return secureRandom;
  553. }
  554. Future<void> verifyEmailViaPassword(
  555. BuildContext context,
  556. SrpAttributes srpAttributes,
  557. String userPassword,
  558. ProgressDialog dialog,
  559. ) async {
  560. late Uint8List keyEncryptionKey;
  561. _logger.finest('Start deriving key');
  562. keyEncryptionKey = await CryptoUtil.deriveKey(
  563. utf8.encode(userPassword) as Uint8List,
  564. CryptoUtil.base642bin(srpAttributes.kekSalt),
  565. srpAttributes.memLimit,
  566. srpAttributes.opsLimit,
  567. );
  568. _logger.finest('keyDerivation done, derive LoginKey');
  569. final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
  570. final Uint8List identity = Uint8List.fromList(
  571. utf8.encode(srpAttributes.srpUserID),
  572. );
  573. _logger.finest('longinKey derivation done');
  574. final Uint8List salt = base64Decode(srpAttributes.srpSalt);
  575. final Uint8List password = loginKey;
  576. final SecureRandom random = _getSecureRandom();
  577. final client = SRP6Client(
  578. group: kDefaultSrpGroup,
  579. digest: Digest('SHA-256'),
  580. random: random,
  581. );
  582. final A = client.generateClientCredentials(salt, identity, password);
  583. final createSessionResponse = await _dio.post(
  584. _config.getHttpEndpoint() + "/users/srp/create-session",
  585. data: {
  586. "srpUserID": srpAttributes.srpUserID,
  587. "srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
  588. },
  589. );
  590. final String sessionID = createSessionResponse.data["sessionID"];
  591. final String srpB = createSessionResponse.data["srpB"];
  592. final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
  593. // ignore: need to calculate secret to get M1, unused_local_variable
  594. final clientS = client.calculateSecret(serverB);
  595. final clientM = client.calculateClientEvidenceMessage();
  596. final response = await _dio.post(
  597. _config.getHttpEndpoint() + "/users/srp/verify-session",
  598. data: {
  599. "sessionID": sessionID,
  600. "srpUserID": srpAttributes.srpUserID,
  601. "srpM1": base64Encode(SRP6Util.encodeBigInt(clientM!)),
  602. },
  603. );
  604. if (response.statusCode == 200) {
  605. Widget page;
  606. final String twoFASessionID = response.data["twoFactorSessionID"];
  607. Configuration.instance.setVolatilePassword(userPassword);
  608. if (twoFASessionID.isNotEmpty) {
  609. setTwoFactor(value: true);
  610. page = TwoFactorAuthenticationPage(twoFASessionID);
  611. } else {
  612. await _saveConfiguration(response);
  613. if (Configuration.instance.getEncryptedToken() != null) {
  614. await Configuration.instance.decryptSecretsAndGetKeyEncKey(
  615. userPassword,
  616. Configuration.instance.getKeyAttributes()!,
  617. keyEncryptionKey: keyEncryptionKey,
  618. );
  619. page = const HomeWidget();
  620. } else {
  621. throw Exception("unexpected response during email verification");
  622. }
  623. }
  624. await dialog.hide();
  625. if (page is HomeWidget) {
  626. Navigator.of(context).popUntil((route) => route.isFirst);
  627. Bus.instance.fire(AccountConfiguredEvent());
  628. } else {
  629. Navigator.of(context).pushAndRemoveUntil(
  630. MaterialPageRoute(
  631. builder: (BuildContext context) {
  632. return page;
  633. },
  634. ),
  635. (route) => route.isFirst,
  636. );
  637. }
  638. } else {
  639. // should never reach here
  640. throw Exception("unexpected response during email verification");
  641. }
  642. }
  643. Future<void> updateKeyAttributes(
  644. KeyAttributes keyAttributes,
  645. Uint8List loginKey,
  646. ) async {
  647. try {
  648. final setKeyRequest = SetKeysRequest(
  649. kekSalt: keyAttributes.kekSalt,
  650. encryptedKey: keyAttributes.encryptedKey,
  651. keyDecryptionNonce: keyAttributes.keyDecryptionNonce,
  652. memLimit: keyAttributes.memLimit!,
  653. opsLimit: keyAttributes.opsLimit!,
  654. );
  655. await registerOrUpdateSrp(loginKey, setKeysRequest: setKeyRequest);
  656. await _config.setKeyAttributes(keyAttributes);
  657. } catch (e) {
  658. _logger.severe(e);
  659. rethrow;
  660. }
  661. }
  662. Future<void> setRecoveryKey(KeyAttributes keyAttributes) async {
  663. try {
  664. final setRecoveryKeyRequest = SetRecoveryKeyRequest(
  665. keyAttributes.masterKeyEncryptedWithRecoveryKey!,
  666. keyAttributes.masterKeyDecryptionNonce!,
  667. keyAttributes.recoveryKeyEncryptedWithMasterKey!,
  668. keyAttributes.recoveryKeyDecryptionNonce!,
  669. );
  670. await _enteDio.put(
  671. "/users/recovery-key",
  672. data: setRecoveryKeyRequest.toMap(),
  673. );
  674. await _config.setKeyAttributes(keyAttributes);
  675. } catch (e) {
  676. _logger.severe(e);
  677. rethrow;
  678. }
  679. }
  680. Future<void> verifyTwoFactor(
  681. BuildContext context,
  682. String sessionID,
  683. String code,
  684. ) async {
  685. final dialog = createProgressDialog(context, S.of(context).authenticating);
  686. await dialog.show();
  687. try {
  688. final response = await _dio.post(
  689. _config.getHttpEndpoint() + "/users/two-factor/verify",
  690. data: {
  691. "sessionID": sessionID,
  692. "code": code,
  693. },
  694. );
  695. await dialog.hide();
  696. if (response.statusCode == 200) {
  697. showShortToast(context, S.of(context).authenticationSuccessful);
  698. await _saveConfiguration(response);
  699. Navigator.of(context).pushAndRemoveUntil(
  700. MaterialPageRoute(
  701. builder: (BuildContext context) {
  702. return const PasswordReentryPage();
  703. },
  704. ),
  705. (route) => route.isFirst,
  706. );
  707. }
  708. } on DioError catch (e) {
  709. await dialog.hide();
  710. _logger.severe(e);
  711. if (e.response != null && e.response!.statusCode == 404) {
  712. showToast(context, "Session expired");
  713. Navigator.of(context).pushAndRemoveUntil(
  714. MaterialPageRoute(
  715. builder: (BuildContext context) {
  716. return const LoginPage();
  717. },
  718. ),
  719. (route) => route.isFirst,
  720. );
  721. } else {
  722. showErrorDialog(
  723. context,
  724. S.of(context).incorrectCode,
  725. S.of(context).authenticationFailedPleaseTryAgain,
  726. );
  727. }
  728. } catch (e) {
  729. await dialog.hide();
  730. _logger.severe(e);
  731. showErrorDialog(
  732. context,
  733. S.of(context).oops,
  734. S.of(context).authenticationFailedPleaseTryAgain,
  735. );
  736. }
  737. }
  738. Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
  739. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  740. await dialog.show();
  741. try {
  742. final response = await _dio.get(
  743. _config.getHttpEndpoint() + "/users/two-factor/recover",
  744. queryParameters: {
  745. "sessionID": sessionID,
  746. },
  747. );
  748. if (response.statusCode == 200) {
  749. Navigator.of(context).pushAndRemoveUntil(
  750. MaterialPageRoute(
  751. builder: (BuildContext context) {
  752. return TwoFactorRecoveryPage(
  753. sessionID,
  754. response.data["encryptedSecret"],
  755. response.data["secretDecryptionNonce"],
  756. );
  757. },
  758. ),
  759. (route) => route.isFirst,
  760. );
  761. }
  762. } on DioError catch (e) {
  763. _logger.severe(e);
  764. if (e.response != null && e.response!.statusCode == 404) {
  765. showToast(context, S.of(context).sessionExpired);
  766. Navigator.of(context).pushAndRemoveUntil(
  767. MaterialPageRoute(
  768. builder: (BuildContext context) {
  769. return const LoginPage();
  770. },
  771. ),
  772. (route) => route.isFirst,
  773. );
  774. } else {
  775. showErrorDialog(
  776. context,
  777. S.of(context).oops,
  778. S.of(context).somethingWentWrongPleaseTryAgain,
  779. );
  780. }
  781. } catch (e) {
  782. _logger.severe(e);
  783. showErrorDialog(
  784. context,
  785. S.of(context).oops,
  786. S.of(context).somethingWentWrongPleaseTryAgain,
  787. );
  788. } finally {
  789. await dialog.hide();
  790. }
  791. }
  792. Future<void> removeTwoFactor(
  793. BuildContext context,
  794. String sessionID,
  795. String recoveryKey,
  796. String encryptedSecret,
  797. String secretDecryptionNonce,
  798. ) async {
  799. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  800. await dialog.show();
  801. String secret;
  802. try {
  803. if (recoveryKey.contains(' ')) {
  804. if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
  805. throw AssertionError(
  806. 'recovery code should have $mnemonicKeyWordCount words',
  807. );
  808. }
  809. recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
  810. }
  811. secret = CryptoUtil.bin2base64(
  812. await CryptoUtil.decrypt(
  813. CryptoUtil.base642bin(encryptedSecret),
  814. CryptoUtil.hex2bin(recoveryKey.trim()),
  815. CryptoUtil.base642bin(secretDecryptionNonce),
  816. ),
  817. );
  818. } catch (e) {
  819. await dialog.hide();
  820. await showErrorDialog(
  821. context,
  822. S.of(context).incorrectRecoveryKey,
  823. S.of(context).theRecoveryKeyYouEnteredIsIncorrect,
  824. );
  825. return;
  826. }
  827. try {
  828. final response = await _dio.post(
  829. _config.getHttpEndpoint() + "/users/two-factor/remove",
  830. data: {
  831. "sessionID": sessionID,
  832. "secret": secret,
  833. },
  834. );
  835. if (response.statusCode == 200) {
  836. showShortToast(
  837. context,
  838. S.of(context).twofactorAuthenticationSuccessfullyReset,
  839. );
  840. await _saveConfiguration(response);
  841. Navigator.of(context).pushAndRemoveUntil(
  842. MaterialPageRoute(
  843. builder: (BuildContext context) {
  844. return const PasswordReentryPage();
  845. },
  846. ),
  847. (route) => route.isFirst,
  848. );
  849. }
  850. } on DioError catch (e) {
  851. _logger.severe(e);
  852. if (e.response != null && e.response!.statusCode == 404) {
  853. showToast(context, "Session expired");
  854. Navigator.of(context).pushAndRemoveUntil(
  855. MaterialPageRoute(
  856. builder: (BuildContext context) {
  857. return const LoginPage();
  858. },
  859. ),
  860. (route) => route.isFirst,
  861. );
  862. } else {
  863. showErrorDialog(
  864. context,
  865. S.of(context).oops,
  866. S.of(context).somethingWentWrongPleaseTryAgain,
  867. );
  868. }
  869. } catch (e) {
  870. _logger.severe(e);
  871. showErrorDialog(
  872. context,
  873. S.of(context).oops,
  874. S.of(context).somethingWentWrongPleaseTryAgain,
  875. );
  876. } finally {
  877. await dialog.hide();
  878. }
  879. }
  880. Future<void> setupTwoFactor(BuildContext context, Completer completer) async {
  881. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  882. await dialog.show();
  883. try {
  884. final response = await _enteDio.post("/users/two-factor/setup");
  885. await dialog.hide();
  886. unawaited(
  887. routeToPage(
  888. context,
  889. TwoFactorSetupPage(
  890. response.data["secretCode"],
  891. response.data["qrCode"],
  892. completer,
  893. ),
  894. ),
  895. );
  896. } catch (e) {
  897. await dialog.hide();
  898. _logger.severe("Failed to setup tfa", e);
  899. completer.complete();
  900. rethrow;
  901. }
  902. }
  903. Future<bool> enableTwoFactor(
  904. BuildContext context,
  905. String secret,
  906. String code,
  907. ) async {
  908. Uint8List recoveryKey;
  909. try {
  910. recoveryKey = await getOrCreateRecoveryKey(context);
  911. } catch (e) {
  912. showGenericErrorDialog(context: context);
  913. return false;
  914. }
  915. final dialog = createProgressDialog(context, S.of(context).verifying);
  916. await dialog.show();
  917. final encryptionResult =
  918. CryptoUtil.encryptSync(CryptoUtil.base642bin(secret), recoveryKey);
  919. try {
  920. await _enteDio.post(
  921. "/users/two-factor/enable",
  922. data: {
  923. "code": code,
  924. "encryptedTwoFactorSecret":
  925. CryptoUtil.bin2base64(encryptionResult.encryptedData!),
  926. "twoFactorSecretDecryptionNonce":
  927. CryptoUtil.bin2base64(encryptionResult.nonce!),
  928. },
  929. );
  930. await dialog.hide();
  931. Navigator.pop(context);
  932. Bus.instance.fire(TwoFactorStatusChangeEvent(true));
  933. return true;
  934. } catch (e, s) {
  935. await dialog.hide();
  936. _logger.severe(e, s);
  937. if (e is DioError) {
  938. if (e.response != null && e.response!.statusCode == 401) {
  939. showErrorDialog(
  940. context,
  941. S.of(context).incorrectCode,
  942. S.of(context).pleaseVerifyTheCodeYouHaveEntered,
  943. );
  944. return false;
  945. }
  946. }
  947. showErrorDialog(
  948. context,
  949. S.of(context).somethingWentWrong,
  950. S.of(context).pleaseContactSupportIfTheProblemPersists,
  951. );
  952. }
  953. return false;
  954. }
  955. Future<void> disableTwoFactor(BuildContext context) async {
  956. final dialog = createProgressDialog(
  957. context,
  958. S.of(context).disablingTwofactorAuthentication,
  959. );
  960. await dialog.show();
  961. try {
  962. await _enteDio.post(
  963. "/users/two-factor/disable",
  964. );
  965. await dialog.hide();
  966. Bus.instance.fire(TwoFactorStatusChangeEvent(false));
  967. unawaited(
  968. showShortToast(
  969. context,
  970. S.of(context).twofactorAuthenticationHasBeenDisabled,
  971. ),
  972. );
  973. } catch (e) {
  974. await dialog.hide();
  975. _logger.severe("Failed to disabled 2FA", e);
  976. await showErrorDialog(
  977. context,
  978. S.of(context).somethingWentWrong,
  979. S.of(context).pleaseContactSupportIfTheProblemPersists,
  980. );
  981. }
  982. }
  983. Future<bool> fetchTwoFactorStatus() async {
  984. try {
  985. final response = await _enteDio.get("/users/two-factor/status");
  986. setTwoFactor(value: response.data["status"]);
  987. return response.data["status"];
  988. } catch (e) {
  989. _logger.severe("Failed to fetch 2FA status", e);
  990. rethrow;
  991. }
  992. }
  993. Future<Uint8List> getOrCreateRecoveryKey(BuildContext context) async {
  994. final String? encryptedRecoveryKey =
  995. _config.getKeyAttributes()!.recoveryKeyEncryptedWithMasterKey;
  996. if (encryptedRecoveryKey == null || encryptedRecoveryKey.isEmpty) {
  997. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  998. await dialog.show();
  999. try {
  1000. final keyAttributes = await _config.createNewRecoveryKey();
  1001. await setRecoveryKey(keyAttributes);
  1002. await dialog.hide();
  1003. } catch (e, s) {
  1004. await dialog.hide();
  1005. _logger.severe(e, s);
  1006. rethrow;
  1007. }
  1008. }
  1009. final recoveryKey = _config.getRecoveryKey();
  1010. return recoveryKey;
  1011. }
  1012. Future<String?> getPaymentToken() async {
  1013. try {
  1014. final response = await _enteDio.get("/users/payment-token");
  1015. if (response.statusCode == 200) {
  1016. return response.data["paymentToken"];
  1017. } else {
  1018. throw Exception("non 200 ok response");
  1019. }
  1020. } catch (e) {
  1021. _logger.severe("Failed to get payment token", e);
  1022. return null;
  1023. }
  1024. }
  1025. Future<String> getFamiliesToken() async {
  1026. try {
  1027. final response = await _enteDio.get("/users/families-token");
  1028. if (response.statusCode == 200) {
  1029. return response.data["familiesToken"];
  1030. } else {
  1031. throw Exception("non 200 ok response");
  1032. }
  1033. } catch (e, s) {
  1034. _logger.severe("failed to fetch families token", e, s);
  1035. rethrow;
  1036. }
  1037. }
  1038. Future<void> _saveConfiguration(Response response) async {
  1039. await Configuration.instance.setUserID(response.data["id"]);
  1040. if (response.data["encryptedToken"] != null) {
  1041. await Configuration.instance
  1042. .setEncryptedToken(response.data["encryptedToken"]);
  1043. await Configuration.instance.setKeyAttributes(
  1044. KeyAttributes.fromMap(response.data["keyAttributes"]),
  1045. );
  1046. } else {
  1047. await Configuration.instance.setToken(response.data["token"]);
  1048. }
  1049. }
  1050. Future<void> setTwoFactor({
  1051. bool value = false,
  1052. bool fetchTwoFactorStatus = false,
  1053. }) async {
  1054. if (fetchTwoFactorStatus) {
  1055. value = await UserService.instance.fetchTwoFactorStatus();
  1056. }
  1057. _preferences.setBool(keyHasEnabledTwoFactor, value);
  1058. }
  1059. bool hasEnabledTwoFactor() {
  1060. return _preferences.getBool(keyHasEnabledTwoFactor) ?? false;
  1061. }
  1062. bool hasEmailMFAEnabled() {
  1063. final UserDetails? profile = getCachedUserDetails();
  1064. if (profile != null && profile.profileData != null) {
  1065. return profile.profileData!.isEmailMFAEnabled;
  1066. }
  1067. return true;
  1068. }
  1069. Future<void> updateEmailMFA(bool isEnabled) async {
  1070. try {
  1071. await _enteDio.put(
  1072. "/users/email-mfa",
  1073. data: {
  1074. "isEnabled": isEnabled,
  1075. },
  1076. );
  1077. final UserDetails? profile = getCachedUserDetails();
  1078. if (profile != null && profile.profileData != null) {
  1079. profile.profileData!.isEmailMFAEnabled = isEnabled;
  1080. await _preferences.setString(keyUserDetails, profile.toJson());
  1081. }
  1082. } catch (e) {
  1083. _logger.severe("Failed to update email mfa", e);
  1084. rethrow;
  1085. }
  1086. }
  1087. }