ente/lib/services/authenticator_service.dart
2022-11-01 15:29:51 +05:30

226 lines
7.6 KiB
Dart

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';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/events/signed_in_event.dart';
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/local_auth_entity.dart';
import 'package:ente_auth/store/authenticator_db.dart';
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';
class AuthenticatorService {
final _logger = Logger((AuthenticatorService).toString());
final _config = Configuration.instance;
late SharedPreferences _prefs;
late AuthenticatorGateway _gateway;
late AuthenticatorDB _db;
final String _lastEntitySyncTime = "lastEntitySyncTime";
AuthenticatorService._privateConstructor();
static final AuthenticatorService instance =
AuthenticatorService._privateConstructor();
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_db = AuthenticatorDB.instance;
_gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
if (Configuration.instance.hasConfiguredAccount()) {
unawaited(sync());
}
Bus.instance.on<SignedInEvent>().listen((event) {
unawaited(sync());
});
}
Future<Map<int, String>> getAllIDtoStringMap() async {
final List<LocalAuthEntity> result = await _db.getAll();
final Map<int, String> entries = <int, String>{};
if (result.isEmpty) {
return entries;
}
final key = await getOrCreateAuthDataKey();
for (LocalAuthEntity e in result) {
final decryptedValue = await CryptoUtil.decryptChaCha(
Sodium.base642bin(e.encryptedData),
key,
Sodium.base642bin(e.header),
);
entries[e.generatedID] = utf8.decode(decryptedValue);
}
return entries;
}
Future<int> addEntry(String plainText) async {
var key = await getOrCreateAuthDataKey();
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
final insertedID = await _db.insert(encryptedData, header);
unawaited(sync());
return insertedID;
}
Future<void> updateEntry(int generatedID, String plainText) async {
var key = await getOrCreateAuthDataKey();
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
final int affectedRows =
await _db.updateEntry(generatedID, encryptedData, header);
assert(
affectedRows == 1,
"updateEntry should have updated exactly one row",
);
unawaited(sync());
}
Future<void> deleteEntry(int genID) async {
LocalAuthEntity? result = await _db.getEntryByID(genID);
if (result == null) {
_logger.info("No entry found for given id");
return;
}
if (result.id != null) {
await _gateway.deleteEntity(result.id!);
}
await _db.deleteByIDs(generatedIDs: [genID]);
}
Future<void> sync() async {
try {
_logger.info("Sync");
_logger.info("State of DB before sync");
_logger.info("_____");
var entities = await _db.getAll();
for (final entity in entities) {
_logger.info(entity.id);
}
await _remoteToLocalSync();
_logger.info("remote fetch completed");
_logger.info("State of DB after remoteToLocal sync");
_logger.info("_____");
entities = await _db.getAll();
for (final entity in entities) {
_logger.info(entity.id);
}
_logger.info("_____");
await _localToRemoteSync();
_logger.info("State of DB after localToRemote sync");
_logger.info("_____");
entities = await _db.getAll();
for (final entity in entities) {
_logger.info(entity.id);
}
_logger.info("_____");
_logger.info("local push completed");
Bus.instance.fire(CodesUpdatedEvent());
} catch (e) {
_logger.severe("Failed to sync with remote", e);
}
}
Future<void> _remoteToLocalSync() async {
_logger.info('Initiating remote to local sync');
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
const int fetchLimit = 500;
final List<AuthEntity> result =
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
if (result.isEmpty) {
return;
} else {
_logger.info(result.length.toString() + " entries fetched from remote");
}
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
List<String> deletedIDs =
result.where((element) => element.isDeleted).map((e) => e.id).toList();
result.removeWhere((element) => element.isDeleted);
await _db.insertOrReplace(result);
if (deletedIDs.isNotEmpty) {
await _db.deleteByIDs(ids: deletedIDs);
}
_prefs.setInt(_lastEntitySyncTime, maxSyncTime);
if (result.length == fetchLimit) {
await _remoteToLocalSync();
}
}
Future<void> _localToRemoteSync() async {
_logger.info('Initiating local to remote sync');
final List<LocalAuthEntity> result = await _db.getAll();
final List<LocalAuthEntity> pendingUpdate = result
.where((element) => element.shouldSync || element.id == null)
.toList();
_logger.info(
pendingUpdate.length.toString() + " entries to be updated at remote",
);
for (LocalAuthEntity entity in pendingUpdate) {
if (entity.id == null) {
_logger.info("Adding new entry");
final authEntity =
await _gateway.createEntity(entity.encryptedData, entity.header);
await _db.updateLocalEntity(
entity.copyWith(
id: authEntity.id,
shouldSync: false,
),
);
} else {
_logger.info("Updating entry");
await _gateway.updateEntity(
entity.id!,
entity.encryptedData,
entity.header,
);
await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
}
}
}
Future<Uint8List> getOrCreateAuthDataKey() async {
if (_config.getAuthSecretKey() != null) {
return _config.getAuthSecretKey()!;
}
try {
final AuthKey response = await _gateway.getKey();
final authKey = CryptoUtil.decryptSync(
Sodium.base642bin(response.encryptedKey),
_config.getKey(),
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;
}
}
}