FurryChat/lib/utils/famedlysdk_store.dart
2020-10-15 16:51:24 +03:00

297 lines
9 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:localstorage/localstorage.dart';
import 'package:olm/olm.dart' as olm; // needed for migration
import 'package:path_provider/path_provider.dart';
import 'package:random_string/random_string.dart';
import './database/shared.dart';
import 'platform_infos.dart';
Future<LocalStorage> getLocalStorage() async {
final directory = PlatformInfos.isBetaDesktop
? await getApplicationSupportDirectory()
: await getApplicationDocumentsDirectory();
final localStorage = LocalStorage('LocalStorage', directory.path);
await localStorage.ready;
return localStorage;
}
Future<Database> getDatabase(Client client) async {
while (_generateDatabaseLock) {
await Future.delayed(Duration(milliseconds: 50));
}
_generateDatabaseLock = true;
try {
if (_db != null) return _db;
final store = Store();
var password = await store.getItem('database-password');
var needMigration = false;
if (password == null || password.isEmpty) {
needMigration = true;
password = randomString(255);
}
_db = await constructDb(
logStatements: false,
filename: 'moor.sqlite',
password: password,
);
// Check if database is open:
debugPrint((await _db.customSelect('SELECT 1').get()).toString());
if (needMigration) {
debugPrint('[Moor] Start migration');
await migrate(client.clientName, _db, store);
await store.setItem('database-password', password);
}
return _db;
} finally {
_generateDatabaseLock = false;
}
}
Database _db;
bool _generateDatabaseLock = false;
Future<void> migrate(String clientName, Database db, Store store) async {
debugPrint('[Store] attempting old migration to moor...');
final oldKeys = await store.getAllItems();
if (oldKeys == null || oldKeys.isEmpty) {
debugPrint('[Store] empty store!');
return; // we are done!
}
final credentialsStr = oldKeys[clientName];
if (credentialsStr == null || credentialsStr.isEmpty) {
debugPrint('[Store] no credentials found!');
return; // no credentials
}
final Map<String, dynamic> credentials = json.decode(credentialsStr);
if (!credentials.containsKey('homeserver') ||
!credentials.containsKey('token') ||
!credentials.containsKey('userID')) {
debugPrint('[Store] invalid credentials!');
return; // invalid old store, we are done, too!
}
var clientId = 0;
final oldClient = await db.getClient(clientName);
if (oldClient == null) {
clientId = await db.insertClient(
clientName,
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
);
} else {
clientId = oldClient.clientId;
await db.updateClient(
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
clientId,
);
}
await db.clearCache(clientId);
debugPrint('[Store] Inserted/updated client, clientId = ${clientId}');
await db.transaction(() async {
// alright, we stored / updated the client and have the account ID, time to import everything else!
// user_device_keys and user_device_keys_key
debugPrint('[Store] Migrating user device keys...');
final deviceKeysListString = oldKeys['${clientName}.user_device_keys'];
if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) {
Map<String, dynamic> rawUserDeviceKeys =
json.decode(deviceKeysListString);
for (final entry in rawUserDeviceKeys.entries) {
final map = entry.value;
await db.storeUserDeviceKeysInfo(
clientId, map['user_id'], map['outdated']);
for (final rawKey in map['device_keys'].entries) {
final jsonVaue = rawKey.value;
await db.storeUserDeviceKey(
clientId,
jsonVaue['user_id'],
jsonVaue['device_id'],
json.encode(jsonVaue),
jsonVaue['verified'],
jsonVaue['blocked']);
}
}
}
for (final entry in oldKeys.entries) {
final key = entry.key;
final value = entry.value;
if (value == null || value.isEmpty) {
continue;
}
// olm_sessions
final olmSessionsMatch =
RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key);
if (olmSessionsMatch != null) {
if (olmSessionsMatch[1] != credentials['deviceID']) {
continue;
}
debugPrint('[Store] migrating olm sessions...');
final identityKey = json.decode(value);
for (final olmKey in identityKey.entries) {
final identKey = olmKey.key;
final sessions = olmKey.value;
for (final pickle in sessions) {
var sess = olm.Session();
sess.unpickle(credentials['userID'], pickle);
await db.storeOlmSession(
clientId, identKey, sess.session_id(), pickle, null);
sess?.free();
}
}
}
// outbound_group_sessions
final outboundGroupSessionsMatch = RegExp(
r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$')
.firstMatch(key);
if (outboundGroupSessionsMatch != null) {
if (outboundGroupSessionsMatch[1] != credentials['deviceID']) {
continue;
}
final pickle = value;
final roomId = outboundGroupSessionsMatch[2];
debugPrint(
'[Store] Migrating outbound group sessions for room ${roomId}...');
final devicesString = oldKeys[
'/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices'];
var devices = <String>[];
if (devicesString != null) {
devices = List<String>.from(json.decode(devicesString));
}
await db.storeOutboundGroupSession(
clientId,
roomId,
pickle,
json.encode(devices),
DateTime.now().millisecondsSinceEpoch,
0,
);
}
// session_keys
final sessionKeysMatch =
RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$')
.firstMatch(key);
if (sessionKeysMatch != null) {
if (sessionKeysMatch[1] != credentials['deviceID']) {
continue;
}
final roomId = sessionKeysMatch[2];
debugPrint('[Store] Migrating session keys for room ${roomId}...');
final map = json.decode(value);
for (final entry in map.entries) {
await db.storeInboundGroupSession(
clientId,
roomId,
entry.key,
entry.value['inboundGroupSession'],
json.encode(entry.value['content']),
json.encode(entry.value['indexes']),
null,
null);
}
}
}
});
}
// see https://github.com/mogol/flutter_secure_storage/issues/161#issuecomment-704578453
class AsyncMutex {
Completer<void> _completer;
Future<void> lock() async {
while (_completer != null) {
await _completer.future;
}
_completer = Completer<void>();
}
void unlock() {
assert(_completer != null);
final completer = _completer;
_completer = null;
completer.complete();
}
}
class Store {
final LocalStorage storage;
final FlutterSecureStorage secureStorage;
static final _mutex = AsyncMutex();
Store()
: storage = LocalStorage('LocalStorage'),
secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null;
Future<dynamic> getItem(String key) async {
if (!PlatformInfos.isMobile) {
await storage.ready;
try {
return await storage.getItem(key);
} catch (_) {
return null;
}
}
try {
await _mutex.lock();
return await secureStorage.read(key: key);
} catch (_) {
return null;
} finally {
_mutex.unlock();
}
}
Future<void> setItem(String key, String value) async {
if (!PlatformInfos.isMobile) {
await storage.ready;
return await storage.setItem(key, value);
}
if (value == null) {
return await secureStorage.delete(key: key);
} else {
try {
await _mutex.lock();
return await secureStorage.write(key: key, value: value);
} finally {
_mutex.unlock();
}
}
}
Future<Map<String, dynamic>> getAllItems() async {
if (!PlatformInfos.isMobile) {
try {
final rawStorage = await getLocalstorage('LocalStorage');
return json.decode(rawStorage);
} catch (_) {
return {};
}
}
try {
await _mutex.lock();
return await secureStorage.readAll();
} catch (_) {
return {};
} finally {
_mutex.unlock();
}
}
}