authenticator_service.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:math';
  4. import 'package:ente_auth/core/configuration.dart';
  5. import 'package:ente_auth/core/errors.dart';
  6. import 'package:ente_auth/core/event_bus.dart';
  7. import 'package:ente_auth/core/network.dart';
  8. import 'package:ente_auth/events/codes_updated_event.dart';
  9. import 'package:ente_auth/events/signed_in_event.dart';
  10. import 'package:ente_auth/events/trigger_logout_event.dart';
  11. import 'package:ente_auth/gateway/authenticator.dart';
  12. import 'package:ente_auth/models/authenticator/auth_entity.dart';
  13. import 'package:ente_auth/models/authenticator/auth_key.dart';
  14. import 'package:ente_auth/models/authenticator/entity_result.dart';
  15. import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
  16. import 'package:ente_auth/store/authenticator_db.dart';
  17. import 'package:ente_auth/utils/crypto_util.dart';
  18. import 'package:flutter/foundation.dart';
  19. import 'package:flutter_sodium/flutter_sodium.dart';
  20. import 'package:logging/logging.dart';
  21. import 'package:shared_preferences/shared_preferences.dart';
  22. class AuthenticatorService {
  23. final _logger = Logger((AuthenticatorService).toString());
  24. final _config = Configuration.instance;
  25. late SharedPreferences _prefs;
  26. late AuthenticatorGateway _gateway;
  27. late AuthenticatorDB _db;
  28. final String _lastEntitySyncTime = "lastEntitySyncTime";
  29. AuthenticatorService._privateConstructor();
  30. static final AuthenticatorService instance =
  31. AuthenticatorService._privateConstructor();
  32. Future<void> init() async {
  33. _prefs = await SharedPreferences.getInstance();
  34. _db = AuthenticatorDB.instance;
  35. _gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
  36. if (Configuration.instance.hasConfiguredAccount()) {
  37. unawaited(sync());
  38. }
  39. Bus.instance.on<SignedInEvent>().listen((event) {
  40. unawaited(sync());
  41. });
  42. }
  43. Future<List<EntityResult>> getEntities() async {
  44. final List<LocalAuthEntity> result = await _db.getAll();
  45. final List<EntityResult> entities = [];
  46. if (result.isEmpty) {
  47. return entities;
  48. }
  49. final key = await getOrCreateAuthDataKey();
  50. for (LocalAuthEntity e in result) {
  51. try {
  52. final decryptedValue = await CryptoUtil.decryptChaCha(
  53. Sodium.base642bin(e.encryptedData),
  54. key,
  55. Sodium.base642bin(e.header),
  56. );
  57. final hasSynced = !(e.id == null || e.shouldSync);
  58. entities.add(
  59. EntityResult(
  60. e.generatedID,
  61. utf8.decode(decryptedValue),
  62. hasSynced,
  63. ),
  64. );
  65. } catch (e, s) {
  66. _logger.severe(e, s);
  67. }
  68. }
  69. return entities;
  70. }
  71. Future<int> addEntry(String plainText, bool shouldSync) async {
  72. var key = await getOrCreateAuthDataKey();
  73. final encryptedKeyData = await CryptoUtil.encryptChaCha(
  74. utf8.encode(plainText) as Uint8List,
  75. key,
  76. );
  77. String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
  78. String header = Sodium.bin2base64(encryptedKeyData.header!);
  79. final insertedID = await _db.insert(encryptedData, header);
  80. if (shouldSync) {
  81. unawaited(sync());
  82. }
  83. return insertedID;
  84. }
  85. Future<void> updateEntry(
  86. int generatedID,
  87. String plainText,
  88. bool shouldSync,
  89. ) async {
  90. var key = await getOrCreateAuthDataKey();
  91. final encryptedKeyData = await CryptoUtil.encryptChaCha(
  92. utf8.encode(plainText) as Uint8List,
  93. key,
  94. );
  95. String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
  96. String header = Sodium.bin2base64(encryptedKeyData.header!);
  97. final int affectedRows =
  98. await _db.updateEntry(generatedID, encryptedData, header);
  99. assert(
  100. affectedRows == 1,
  101. "updateEntry should have updated exactly one row",
  102. );
  103. if (shouldSync) {
  104. unawaited(sync());
  105. }
  106. }
  107. Future<void> deleteEntry(int genID) async {
  108. LocalAuthEntity? result = await _db.getEntryByID(genID);
  109. if (result == null) {
  110. _logger.info("No entry found for given id");
  111. return;
  112. }
  113. if (result.id != null) {
  114. await _gateway.deleteEntity(result.id!);
  115. }
  116. await _db.deleteByIDs(generatedIDs: [genID]);
  117. }
  118. Future<void> sync() async {
  119. try {
  120. _logger.info("Sync");
  121. await _remoteToLocalSync();
  122. _logger.info("remote fetch completed");
  123. await _localToRemoteSync();
  124. _logger.info("local push completed");
  125. Bus.instance.fire(CodesUpdatedEvent());
  126. } on UnauthorizedError {
  127. if ((await _db.removeSyncedData()) > 0) {
  128. Bus.instance.fire(CodesUpdatedEvent());
  129. }
  130. debugPrint("Firing logout event");
  131. Bus.instance.fire(TriggerLogoutEvent());
  132. } catch (e) {
  133. _logger.severe("Failed to sync with remote", e);
  134. }
  135. }
  136. Future<void> _remoteToLocalSync() async {
  137. _logger.info('Initiating remote to local sync');
  138. final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
  139. _logger.info("Current sync is " + lastSyncTime.toString());
  140. const int fetchLimit = 500;
  141. final List<AuthEntity> result =
  142. await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
  143. _logger.info(result.length.toString() + " entries fetched from remote");
  144. if (result.isEmpty) {
  145. return;
  146. }
  147. final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
  148. List<String> deletedIDs =
  149. result.where((element) => element.isDeleted).map((e) => e.id).toList();
  150. _logger.info(deletedIDs.length.toString() + " entries deleted");
  151. result.removeWhere((element) => element.isDeleted);
  152. await _db.insertOrReplace(result);
  153. if (deletedIDs.isNotEmpty) {
  154. await _db.deleteByIDs(ids: deletedIDs);
  155. }
  156. _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
  157. _logger.info("Setting synctime to " + maxSyncTime.toString());
  158. if (result.length == fetchLimit) {
  159. _logger.info("Diff limit reached, pulling again");
  160. await _remoteToLocalSync();
  161. }
  162. }
  163. Future<void> _localToRemoteSync() async {
  164. _logger.info('Initiating local to remote sync');
  165. final List<LocalAuthEntity> result = await _db.getAll();
  166. final List<LocalAuthEntity> pendingUpdate = result
  167. .where((element) => element.shouldSync || element.id == null)
  168. .toList();
  169. _logger.info(
  170. pendingUpdate.length.toString() + " entries to be updated at remote",
  171. );
  172. for (LocalAuthEntity entity in pendingUpdate) {
  173. if (entity.id == null) {
  174. _logger.info("Adding new entry");
  175. final authEntity =
  176. await _gateway.createEntity(entity.encryptedData, entity.header);
  177. await _db.updateLocalEntity(
  178. entity.copyWith(
  179. id: authEntity.id,
  180. shouldSync: false,
  181. ),
  182. );
  183. } else {
  184. _logger.info("Updating entry");
  185. await _gateway.updateEntity(
  186. entity.id!,
  187. entity.encryptedData,
  188. entity.header,
  189. );
  190. await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
  191. }
  192. }
  193. if (pendingUpdate.isNotEmpty) {
  194. _logger.info("Initiating remote sync since local entries were pushed");
  195. await _remoteToLocalSync();
  196. }
  197. }
  198. Future<Uint8List> getOrCreateAuthDataKey() async {
  199. if (_config.getAuthSecretKey() != null) {
  200. return _config.getAuthSecretKey()!;
  201. }
  202. try {
  203. final AuthKey response = await _gateway.getKey();
  204. final authKey = CryptoUtil.decryptSync(
  205. Sodium.base642bin(response.encryptedKey),
  206. _config.getKey(),
  207. Sodium.base642bin(response.header),
  208. );
  209. await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
  210. return authKey;
  211. } on AuthenticatorKeyNotFound catch (e) {
  212. _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
  213. final key = CryptoUtil.generateKey();
  214. final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
  215. await _gateway.createKey(
  216. Sodium.bin2base64(encryptedKeyData.encryptedData!),
  217. Sodium.bin2base64(encryptedKeyData.nonce!),
  218. );
  219. await _config.setAuthSecretKey(Sodium.bin2base64(key));
  220. return key;
  221. } catch (e, s) {
  222. _logger.severe("Failed to getOrCreateAuthDataKey", e, s);
  223. rethrow;
  224. }
  225. }
  226. }