/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:hive/hive.dart';
import 'package:matrix/encryption/utils/olm_session.dart';
import 'package:matrix/encryption/utils/outbound_group_session.dart';
import 'package:matrix/encryption/utils/ssss_cache.dart';
import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/database/zone_transaction_mixin.dart';
import 'package:matrix/src/utils/copy_map.dart';
import 'package:matrix/src/utils/queued_to_device_event.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';
/// This is a basic database for the Matrix SDK using the hive store. You need
/// to make sure that you perform `Hive.init()` or `Hive.flutterInit()` before
/// you use this.
///
/// This database does not support file caching!
@Deprecated(
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!',
)
class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
static const int version = 6;
final String name;
late Box _clientBox;
late Box _accountDataBox;
late Box _roomsBox;
late Box _toDeviceQueueBox;
/// Key is a tuple as MultiKey(roomId, type) where stateKey can be
/// an empty string.
late LazyBox _roomStateBox;
/// Key is a tuple as MultiKey(roomId, userId)
late LazyBox _roomMembersBox;
/// Key is a tuple as MultiKey(roomId, type)
late LazyBox _roomAccountDataBox;
late LazyBox _inboundGroupSessionsBox;
late LazyBox _outboundGroupSessionsBox;
late LazyBox _olmSessionsBox;
/// Key is a tuple as MultiKey(userId, deviceId)
late LazyBox _userDeviceKeysBox;
/// Key is the user ID as a String
late LazyBox _userDeviceKeysOutdatedBox;
/// Key is a tuple as MultiKey(userId, publicKey)
late LazyBox _userCrossSigningKeysBox;
late LazyBox _ssssCacheBox;
late LazyBox _presencesBox;
/// Key is a tuple as Multikey(roomId, fragmentId) while the default
/// fragmentId is an empty String
late LazyBox _timelineFragmentsBox;
/// Key is a tuple as MultiKey(roomId, eventId)
late LazyBox _eventsBox;
/// Key is a tuple as MultiKey(userId, deviceId)
late LazyBox _seenDeviceIdsBox;
late LazyBox _seenDeviceKeysBox;
String get _clientBoxName => '$name.box.client';
String get _accountDataBoxName => '$name.box.account_data';
String get _roomsBoxName => '$name.box.rooms';
String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
String get _roomStateBoxName => '$name.box.room_states';
String get _roomMembersBoxName => '$name.box.room_members';
String get _roomAccountDataBoxName => '$name.box.room_account_data';
String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
String get _outboundGroupSessionsBoxName =>
'$name.box.outbound_group_session';
String get _olmSessionsBoxName => '$name.box.olm_session';
String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
String get _userDeviceKeysOutdatedBoxName =>
'$name.box.user_device_keys_outdated';
String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
String get _ssssCacheBoxName => '$name.box.ssss_cache';
String get _presencesBoxName => '$name.box.presences';
String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
String get _eventsBoxName => '$name.box.events';
String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
final HiveCipher? encryptionCipher;
FamedlySdkHiveDatabase(this.name, {this.encryptionCipher});
@override
int get maxFileSize => 0;
Future _actionOnAllBoxes(Future Function(BoxBase box) action) =>
Future.wait([
action(_clientBox),
action(_accountDataBox),
action(_roomsBox),
action(_roomStateBox),
action(_roomMembersBox),
action(_toDeviceQueueBox),
action(_roomAccountDataBox),
action(_inboundGroupSessionsBox),
action(_outboundGroupSessionsBox),
action(_olmSessionsBox),
action(_userDeviceKeysBox),
action(_userDeviceKeysOutdatedBox),
action(_userCrossSigningKeysBox),
action(_ssssCacheBox),
action(_presencesBox),
action(_timelineFragmentsBox),
action(_eventsBox),
action(_seenDeviceIdsBox),
action(_seenDeviceKeysBox),
]);
Future open() async {
_clientBox = await Hive.openBox(
_clientBoxName,
encryptionCipher: encryptionCipher,
);
_accountDataBox = await Hive.openBox(
_accountDataBoxName,
encryptionCipher: encryptionCipher,
);
_roomsBox = await Hive.openBox(
_roomsBoxName,
encryptionCipher: encryptionCipher,
);
_roomStateBox = await Hive.openLazyBox(
_roomStateBoxName,
encryptionCipher: encryptionCipher,
);
_roomMembersBox = await Hive.openLazyBox(
_roomMembersBoxName,
encryptionCipher: encryptionCipher,
);
_toDeviceQueueBox = await Hive.openBox(
_toDeviceQueueBoxName,
encryptionCipher: encryptionCipher,
);
_roomAccountDataBox = await Hive.openLazyBox(
_roomAccountDataBoxName,
encryptionCipher: encryptionCipher,
);
_inboundGroupSessionsBox = await Hive.openLazyBox(
_inboundGroupSessionsBoxName,
encryptionCipher: encryptionCipher,
);
_outboundGroupSessionsBox = await Hive.openLazyBox(
_outboundGroupSessionsBoxName,
encryptionCipher: encryptionCipher,
);
_olmSessionsBox = await Hive.openLazyBox(
_olmSessionsBoxName,
encryptionCipher: encryptionCipher,
);
_userDeviceKeysBox = await Hive.openLazyBox(
_userDeviceKeysBoxName,
encryptionCipher: encryptionCipher,
);
_userDeviceKeysOutdatedBox = await Hive.openLazyBox(
_userDeviceKeysOutdatedBoxName,
encryptionCipher: encryptionCipher,
);
_userCrossSigningKeysBox = await Hive.openLazyBox(
_userCrossSigningKeysBoxName,
encryptionCipher: encryptionCipher,
);
_ssssCacheBox = await Hive.openLazyBox(
_ssssCacheBoxName,
encryptionCipher: encryptionCipher,
);
_presencesBox = await Hive.openLazyBox(
_presencesBoxName,
encryptionCipher: encryptionCipher,
);
_timelineFragmentsBox = await Hive.openLazyBox(
_timelineFragmentsBoxName,
encryptionCipher: encryptionCipher,
);
_eventsBox = await Hive.openLazyBox(
_eventsBoxName,
encryptionCipher: encryptionCipher,
);
_seenDeviceIdsBox = await Hive.openLazyBox(
_seenDeviceIdsBoxName,
encryptionCipher: encryptionCipher,
);
_seenDeviceKeysBox = await Hive.openLazyBox(
_seenDeviceKeysBoxName,
encryptionCipher: encryptionCipher,
);
// Check version and check if we need a migration
final currentVersion = (await _clientBox.get('version') as int?);
if (currentVersion == null) {
await _clientBox.put('version', version);
} else if (currentVersion != version) {
await _migrateFromVersion(currentVersion);
}
return;
}
Future _migrateFromVersion(int currentVersion) async {
Logs().i('Migrate Hive database from version $currentVersion to $version');
if (version == 5) {
for (final key in _userDeviceKeysBox.keys) {
try {
final raw = await _userDeviceKeysBox.get(key) as Map;
if (!raw.containsKey('keys')) continue;
final deviceKeys = DeviceKeys.fromJson(
convertToJson(raw),
Client(''),
);
await addSeenDeviceId(
deviceKeys.userId,
deviceKeys.deviceId!,
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!,
);
await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
await addSeenPublicKey(
deviceKeys.curve25519Key!,
deviceKeys.deviceId!,
);
} catch (e) {
Logs().w('Can not migrate device $key', e);
}
}
}
await clearCache();
await _clientBox.put('version', version);
}
@override
Future clear() async {
Logs().i('Clear and close hive database...');
await _actionOnAllBoxes((box) async {
try {
await box.deleteAll(box.keys);
await box.close();
} catch (e) {
Logs().v('Unable to clear box ${box.name}', e);
await box.deleteFromDisk();
}
});
return;
}
@override
Future clearCache() async {
await _roomsBox.deleteAll(_roomsBox.keys);
await _accountDataBox.deleteAll(_accountDataBox.keys);
await _roomAccountDataBox.deleteAll(_roomAccountDataBox.keys);
await _roomStateBox.deleteAll(_roomStateBox.keys);
await _roomMembersBox.deleteAll(_roomMembersBox.keys);
await _eventsBox.deleteAll(_eventsBox.keys);
await _timelineFragmentsBox.deleteAll(_timelineFragmentsBox.keys);
await _outboundGroupSessionsBox.deleteAll(_outboundGroupSessionsBox.keys);
await _presencesBox.deleteAll(_presencesBox.keys);
await _clientBox.delete('prev_batch');
}
@override
Future clearSSSSCache() async {
await _ssssCacheBox.deleteAll(_ssssCacheBox.keys);
}
@override
Future close() => _actionOnAllBoxes((box) => box.close());
@override
Future deleteFromToDeviceQueue(int id) async {
await _toDeviceQueueBox.delete(id);
return;
}
@override
Future deleteOldFiles(int savedAt) async {
return;
}
@override
Future forgetRoom(String roomId) async {
await _timelineFragmentsBox.delete(MultiKey(roomId, '').toString());
for (final key in _eventsBox.keys) {
final multiKey = MultiKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _eventsBox.delete(key);
}
for (final key in _roomStateBox.keys) {
final multiKey = MultiKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomStateBox.delete(key);
}
for (final key in _roomMembersBox.keys) {
final multiKey = MultiKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomMembersBox.delete(key);
}
for (final key in _roomAccountDataBox.keys) {
final multiKey = MultiKey.fromString(key);
if (multiKey.parts.first != roomId) continue;
await _roomAccountDataBox.delete(key);
}
await _roomsBox.delete(roomId.toHiveKey);
}
@override
Future