ente/auth/lib/services/authenticator_service.dart

287 lines
9.5 KiB
Dart
Raw Normal View History

2022-11-01 06:13:06 +00:00
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/core/event_bus.dart';
2022-11-01 07:02:50 +00:00
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/events/signed_in_event.dart';
2023-04-04 10:16:18 +00:00
import 'package:ente_auth/events/trigger_logout_event.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/gateway/authenticator.dart';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
import 'package:ente_auth/models/authenticator/auth_key.dart';
import 'package:ente_auth/models/authenticator/entity_result.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
import 'package:ente_auth/store/authenticator_db.dart';
2023-09-04 10:31:59 +00:00
import 'package:ente_auth/store/offline_authenticator_db.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
2023-09-04 10:31:59 +00:00
enum AccountMode {
online,
offline,
}
2023-09-04 10:31:59 +00:00
extension on AccountMode {
bool get isOnline => this == AccountMode.online;
bool get isOffline => this == AccountMode.offline;
}
2022-11-01 06:13:06 +00:00
class AuthenticatorService {
final _logger = Logger((AuthenticatorService).toString());
final _config = Configuration.instance;
late SharedPreferences _prefs;
late AuthenticatorGateway _gateway;
late AuthenticatorDB _db;
2023-09-04 10:31:59 +00:00
late OfflineAuthenticatorDB _offlineDb;
2022-11-01 06:13:06 +00:00
final String _lastEntitySyncTime = "lastEntitySyncTime";
AuthenticatorService._privateConstructor();
static final AuthenticatorService instance =
AuthenticatorService._privateConstructor();
2023-09-04 10:31:59 +00:00
AccountMode getAccountMode() {
return Configuration.instance.hasOptedForOfflineMode() &&
!Configuration.instance.hasConfiguredAccount()
? AccountMode.offline
: AccountMode.online;
}
2022-11-01 06:13:06 +00:00
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_db = AuthenticatorDB.instance;
2023-09-04 10:31:59 +00:00
_offlineDb = OfflineAuthenticatorDB.instance;
_gateway = AuthenticatorGateway();
2022-11-01 06:13:06 +00:00
if (Configuration.instance.hasConfiguredAccount()) {
2023-09-04 10:31:59 +00:00
unawaited(onlineSync());
2022-11-01 06:13:06 +00:00
}
2022-11-01 07:02:50 +00:00
Bus.instance.on<SignedInEvent>().listen((event) {
2023-09-04 10:31:59 +00:00
unawaited(onlineSync());
2022-11-01 06:13:06 +00:00
});
}
2023-09-04 10:31:59 +00:00
Future<List<EntityResult>> getEntities(AccountMode mode) async {
final List<LocalAuthEntity> result =
mode.isOnline ? await _db.getAll() : await _offlineDb.getAll();
final List<EntityResult> entities = [];
2022-11-01 06:13:06 +00:00
if (result.isEmpty) {
return entities;
2022-11-01 06:13:06 +00:00
}
2023-09-04 10:31:59 +00:00
final key = await getOrCreateAuthDataKey(mode);
2022-11-01 06:13:06 +00:00
for (LocalAuthEntity e in result) {
2022-11-15 11:56:59 +00:00
try {
final decryptedValue = await CryptoUtil.decryptChaCha(
Sodium.base642bin(e.encryptedData),
key,
Sodium.base642bin(e.header),
);
final hasSynced = !(e.id == null || e.shouldSync);
entities.add(
EntityResult(
e.generatedID,
utf8.decode(decryptedValue),
hasSynced,
),
);
2022-11-15 11:56:59 +00:00
} catch (e, s) {
2022-11-15 11:57:12 +00:00
_logger.severe(e, s);
2022-11-15 11:56:59 +00:00
}
2022-11-01 06:13:06 +00:00
}
return entities;
2022-11-01 06:13:06 +00:00
}
2023-09-04 10:31:59 +00:00
Future<int> addEntry(
String plainText,
bool shouldSync,
AccountMode accountMode,
) async {
var key = await getOrCreateAuthDataKey(accountMode);
2022-11-01 06:13:06 +00:00
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
2023-09-04 10:31:59 +00:00
final insertedID = accountMode.isOnline
? await _db.insert(encryptedData, header)
: await _offlineDb.insert(encryptedData, header);
if (shouldSync) {
2023-09-04 10:31:59 +00:00
unawaited(onlineSync());
}
2022-11-01 06:13:06 +00:00
return insertedID;
}
2022-11-22 07:19:39 +00:00
Future<void> updateEntry(
int generatedID,
String plainText,
bool shouldSync,
2023-09-04 10:31:59 +00:00
AccountMode accountMode,
2022-11-22 07:19:39 +00:00
) async {
2023-09-04 10:31:59 +00:00
var key = await getOrCreateAuthDataKey(accountMode);
2022-11-01 06:13:06 +00:00
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
2023-09-04 10:31:59 +00:00
final int affectedRows = accountMode.isOnline
? await _db.updateEntry(generatedID, encryptedData, header)
: await _offlineDb.updateEntry(generatedID, encryptedData, header);
2022-11-01 06:13:06 +00:00
assert(
affectedRows == 1,
"updateEntry should have updated exactly one row",
);
2022-11-22 07:19:39 +00:00
if (shouldSync) {
2023-09-04 10:31:59 +00:00
unawaited(onlineSync());
2022-11-22 07:19:39 +00:00
}
2022-11-01 06:13:06 +00:00
}
2023-09-04 10:31:59 +00:00
Future<void> deleteEntry(int genID, AccountMode accountMode) async {
LocalAuthEntity? result = accountMode.isOnline
? await _db.getEntryByID(genID)
: await _offlineDb.getEntryByID(genID);
2022-11-01 06:13:06 +00:00
if (result == null) {
_logger.info("No entry found for given id");
return;
}
2023-09-04 10:31:59 +00:00
if (result.id != null && accountMode.isOnline) {
2022-11-01 06:13:06 +00:00
await _gateway.deleteEntity(result.id!);
2023-09-04 10:31:59 +00:00
} else {
debugPrint("Skipping delete since account mode is offline");
}
if (accountMode.isOnline) {
2023-09-04 10:31:59 +00:00
await _db.deleteByIDs(generatedIDs: [genID]);
} else {
await _offlineDb.deleteByIDs(generatedIDs: [genID]);
2022-11-01 06:13:06 +00:00
}
}
2023-09-05 02:42:52 +00:00
Future<bool> onlineSync() async {
2022-11-01 06:13:06 +00:00
try {
if (getAccountMode().isOffline) {
2023-09-04 10:31:59 +00:00
debugPrint("Skipping sync since account mode is offline");
2023-09-05 02:42:52 +00:00
return false;
2023-09-04 10:31:59 +00:00
}
2022-11-01 06:13:06 +00:00
_logger.info("Sync");
await _remoteToLocalSync();
2022-11-01 07:02:50 +00:00
_logger.info("remote fetch completed");
2022-11-01 06:13:06 +00:00
await _localToRemoteSync();
2022-11-01 07:02:50 +00:00
_logger.info("local push completed");
Bus.instance.fire(CodesUpdatedEvent());
2023-09-05 02:42:52 +00:00
return true;
2023-04-04 10:16:18 +00:00
} on UnauthorizedError {
2023-04-04 10:36:51 +00:00
if ((await _db.removeSyncedData()) > 0) {
Bus.instance.fire(CodesUpdatedEvent());
}
2023-04-04 10:16:18 +00:00
debugPrint("Firing logout event");
2023-04-04 10:36:51 +00:00
2023-04-04 10:16:18 +00:00
Bus.instance.fire(TriggerLogoutEvent());
2023-09-05 02:42:52 +00:00
return false;
2022-11-01 06:13:06 +00:00
} catch (e) {
_logger.severe("Failed to sync with remote", e);
2023-09-05 02:42:52 +00:00
return false;
2022-11-01 06:13:06 +00:00
}
}
Future<void> _remoteToLocalSync() async {
_logger.info('Initiating remote to local sync');
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
2023-04-04 10:16:18 +00:00
_logger.info("Current sync is " + lastSyncTime.toString());
2022-11-01 06:13:06 +00:00
const int fetchLimit = 500;
final List<AuthEntity> result =
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
2022-11-22 08:36:25 +00:00
_logger.info(result.length.toString() + " entries fetched from remote");
2022-11-01 06:13:06 +00:00
if (result.isEmpty) {
return;
}
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
List<String> deletedIDs =
result.where((element) => element.isDeleted).map((e) => e.id).toList();
2022-11-22 08:36:25 +00:00
_logger.info(deletedIDs.length.toString() + " entries deleted");
2022-11-01 06:13:06 +00:00
result.removeWhere((element) => element.isDeleted);
await _db.insertOrReplace(result);
if (deletedIDs.isNotEmpty) {
await _db.deleteByIDs(ids: deletedIDs);
}
2024-03-07 08:16:05 +00:00
await _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
2022-11-22 08:36:25 +00:00
_logger.info("Setting synctime to " + maxSyncTime.toString());
2022-11-01 06:13:06 +00:00
if (result.length == fetchLimit) {
2022-11-22 08:36:25 +00:00
_logger.info("Diff limit reached, pulling again");
2022-11-01 06:13:06 +00:00
await _remoteToLocalSync();
}
}
Future<void> _localToRemoteSync() async {
2022-11-01 07:02:50 +00:00
_logger.info('Initiating local to remote sync');
2022-11-01 06:13:06 +00:00
final List<LocalAuthEntity> result = await _db.getAll();
final List<LocalAuthEntity> pendingUpdate = result
.where((element) => element.shouldSync || element.id == null)
.toList();
2022-11-01 07:02:50 +00:00
_logger.info(
pendingUpdate.length.toString() + " entries to be updated at remote",
);
2022-11-01 06:13:06 +00:00
for (LocalAuthEntity entity in pendingUpdate) {
if (entity.id == null) {
_logger.info("Adding new entry");
final authEntity =
await _gateway.createEntity(entity.encryptedData, entity.header);
2022-11-01 08:58:14 +00:00
await _db.updateLocalEntity(
entity.copyWith(
id: authEntity.id,
shouldSync: false,
),
);
2022-11-01 06:13:06 +00:00
} else {
_logger.info("Updating entry");
await _gateway.updateEntity(
entity.id!,
entity.encryptedData,
entity.header,
);
2022-11-01 08:58:14 +00:00
await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
2022-11-01 06:13:06 +00:00
}
}
if (pendingUpdate.isNotEmpty) {
2022-11-22 08:36:25 +00:00
_logger.info("Initiating remote sync since local entries were pushed");
await _remoteToLocalSync();
}
2022-11-01 06:13:06 +00:00
}
2023-09-04 10:31:59 +00:00
Future<Uint8List> getOrCreateAuthDataKey(AccountMode mode) async {
if (mode.isOffline) {
2023-09-04 10:31:59 +00:00
return _config.getOfflineSecretKey()!;
}
2022-11-01 06:13:06 +00:00
if (_config.getAuthSecretKey() != null) {
return _config.getAuthSecretKey()!;
}
try {
final AuthKey response = await _gateway.getKey();
final authKey = CryptoUtil.decryptSync(
Sodium.base642bin(response.encryptedKey),
2023-07-20 10:29:23 +00:00
_config.getKey()!,
2022-11-01 06:13:06 +00:00
Sodium.base642bin(response.header),
);
await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
return authKey;
} on AuthenticatorKeyNotFound catch (e) {
_logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
final key = CryptoUtil.generateKey();
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
await _gateway.createKey(
Sodium.bin2base64(encryptedKeyData.encryptedData!),
Sodium.bin2base64(encryptedKeyData.nonce!),
);
await _config.setAuthSecretKey(Sodium.bin2base64(key));
return key;
} catch (e, s) {
_logger.severe("Failed to getOrCreateAuthDataKey", e, s);
rethrow;
}
}
}