Merge pull request 'OMEMO Rework' (#286) from omemo-rework into master

Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/286
This commit is contained in:
PapaTutuWawa 2023-06-21 17:48:01 +00:00
commit 8913977c0a
34 changed files with 943 additions and 904 deletions

View File

@ -170,7 +170,14 @@
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
"stickerSettings": "Sticker settings",
"newDeviceMessage": "${title} added a new encryption device",
"newDeviceMessage": {
"one": "A new device has been added",
"other": "Multiple new devices have been added"
},
"replacedDeviceMessage": {
"one": "A device has been changed",
"other": "Multiple devices have been added"
},
"messageHint": "Send a message...",
"sendImages": "Send images",
"sendFiles": "Send files",

View File

@ -170,7 +170,14 @@
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
"stickerSettings": "Stickereinstellungen",
"newDeviceMessage": "${title} hat ein neues Verschlüsselungsgerät hinzugefügt",
"newDeviceMessage": {
"one": "Ein neues Gerät wurde hinzugefügt",
"other": "Mehrere neue Geräte wurden hinzugefügt"
},
"replacedDeviceMessage": {
"one": "Ein Gerät hat sich verändert",
"other": "Mehrere Geräte haben sich verändert"
},
"messageHint": "Nachricht senden...",
"sendImages": "Bilder senden",
"sendFiles": "Dateien senden",

View File

@ -281,7 +281,8 @@ class ContactsService {
return cs.updateConversation(
contact.jid,
contactId: contact.id,
contactAvatarPath: contact.thumbnail != null ? contactAvatarPath : null,
contactAvatarPath:
contact.thumbnail != null ? contactAvatarPath : null,
contactDisplayName: contact.displayName,
);
},

View File

@ -3,13 +3,6 @@ const messagesTable = 'Messages';
const rosterTable = 'RosterItems';
const mediaTable = 'SharedMedia';
const preferenceTable = 'Preferences';
const omemoDeviceTable = 'OmemoDevices';
const omemoDeviceListTable = 'OmemoDeviceList';
const omemoRatchetsTable = 'OmemoSessions';
const omemoTrustCacheTable = 'OmemoTrustCacheList';
const omemoTrustDeviceListTable = 'OmemoTrustDeviceList';
const omemoTrustEnableListTable = 'OmemoTrustEnableList';
const omemoFingerprintCache = 'OmemoFingerprintCache';
const xmppStateTable = 'XmppState';
const contactsTable = 'Contacts';
const stickersTable = 'Stickers';
@ -19,6 +12,10 @@ const subscriptionsTable = 'SubscriptionRequests';
const fileMetadataTable = 'FileMetadata';
const fileMetadataHashesTable = 'FileMetadataHashes';
const reactionsTable = 'Reactions';
const omemoDevicesTable = 'OmemoDevices';
const omemoDeviceListTable = 'OmemoDeviceList';
const omemoRatchetsTable = 'OmemoRatchets';
const omemoTrustTable = 'OmemoTrustTable';
const typeString = 0;
const typeInt = 1;

View File

@ -18,7 +18,8 @@ Future<void> createDatabase(Database db, int version) async {
);
// Messages
await db.execute('''
await db.execute(
'''
CREATE TABLE $messagesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL,
@ -46,13 +47,15 @@ Future<void> createDatabase(Database db, int version) async {
pseudoMessageData TEXT,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
)''');
)''',
);
await db.execute(
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
);
// Reactions
await db.execute('''
await db.execute(
'''
CREATE TABLE $reactionsTable (
senderJid TEXT NOT NULL,
emoji TEXT NOT NULL,
@ -60,13 +63,15 @@ Future<void> createDatabase(Database db, int version) async {
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
ON DELETE CASCADE
)''');
)''',
);
await db.execute(
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
);
// File metadata
await db.execute('''
await db.execute(
'''
CREATE TABLE $fileMetadataTable (
id TEXT NOT NULL PRIMARY KEY,
path TEXT,
@ -83,8 +88,10 @@ Future<void> createDatabase(Database db, int version) async {
cipherTextHashes TEXT,
filename TEXT NOT NULL,
size INTEGER
)''');
await db.execute('''
)''',
);
await db.execute(
'''
CREATE TABLE $fileMetadataHashesTable (
algorithm TEXT NOT NULL,
value TEXT NOT NULL,
@ -92,7 +99,8 @@ Future<void> createDatabase(Database db, int version) async {
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
ON DELETE CASCADE
)''');
)''',
);
await db.execute(
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
);
@ -103,7 +111,8 @@ Future<void> createDatabase(Database db, int version) async {
CREATE TABLE $conversationsTable (
jid TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
avatarPath TEXT NOT NULL,
avatarHash TEXT,
type TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
@ -124,11 +133,13 @@ Future<void> createDatabase(Database db, int version) async {
);
// Contacts
await db.execute('''
await db.execute(
'''
CREATE TABLE $contactsTable (
id TEXT PRIMARY KEY,
jid TEXT NOT NULL
)''');
)''',
);
// Roster
await db.execute(
@ -137,7 +148,7 @@ Future<void> createDatabase(Database db, int version) async {
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
avatarPath TEXT NOT NULL,
avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL,
ask TEXT NOT NULL,
@ -188,72 +199,58 @@ Future<void> createDatabase(Database db, int version) async {
// OMEMO
await db.execute(
'''
CREATE TABLE $omemoRatchetsTable (
id INTEGER NOT NULL,
jid TEXT NOT NULL,
dhs TEXT NOT NULL,
dhs_pub TEXT NOT NULL,
dhr TEXT,
rk TEXT NOT NULL,
cks TEXT,
ckr TEXT,
ns INTEGER NOT NULL,
nr INTEGER NOT NULL,
pn INTEGER NOT NULL,
ik_pub TEXT NOT NULL,
session_ad TEXT NOT NULL,
acknowledged INTEGER NOT NULL,
mkskipped TEXT NOT NULL,
kex_timestamp INTEGER NOT NULL,
kex TEXT,
PRIMARY KEY (jid, id)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustCacheTable (
key TEXT PRIMARY KEY NOT NULL,
trust INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustDeviceListTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustEnableListTable (
key TEXT PRIMARY KEY NOT NULL,
enabled INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceTable (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
data TEXT NOT NULL,
PRIMARY KEY (jid, id)
CREATE TABLE $omemoDevicesTable (
jid TEXT NOT NULL PRIMARY KEY,
id INTEGER NOT NULL,
ikPub TEXT NOT NULL,
ik TEXT NOT NULL,
spkPub TEXT NOT NULL,
spk TEXT NOT NULL,
spkId INTEGER NOT NULL,
spkSig TEXT NOT NULL,
oldSpkPub TEXT,
oldSpk TEXT,
oldSpkId INTEGER,
opks TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceListTable (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
PRIMARY KEY (jid, id)
jid TEXT NOT NULL PRIMARY KEY,
devices TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoFingerprintCache (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
fingerprint TEXT NOT NULL,
PRIMARY KEY (jid, id)
CREATE TABLE $omemoRatchetsTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL,
dhsPub TEXT NOT NULL,
dhs TEXT NOT NULL,
dhrPub TEXT,
rk TEXT NOT NULL,
cks TEXT,
ckr TEXT,
ns INTEGER NOT NULL,
nr INTEGER NOT NULL,
pn INTEGER NOT NULL,
ik TEXT NOT NULL,
ad TEXT NOT NULL,
skipped TEXT NOT NULL,
kex TEXT NOT NULL,
acked INTEGER NOT NULL,
PRIMARY KEY (jid, device)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL,
trust INTEGER NOT NULL,
enabled INTEGER NOT NULL,
PRIMARY KEY (jid, device)
)''',
);

View File

@ -42,6 +42,8 @@ import 'package:moxxyv2/service/database/migrations/0002_reactions_2.dart';
import 'package:moxxyv2/service/database/migrations/0002_shared_media.dart';
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.dart';
import 'package:moxxyv2/service/database/migrations/0003_avatar_hashes.dart';
import 'package:moxxyv2/service/database/migrations/0003_new_omemo.dart';
import 'package:moxxyv2/service/database/migrations/0003_new_omemo_pseudo_messages.dart';
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
import 'package:path/path.dart' as path;
import 'package:random_string/random_string.dart';
@ -148,6 +150,8 @@ const List<DatabaseMigration<Database>> migrations = [
DatabaseMigration(37, upgradeFromV36ToV37),
DatabaseMigration(38, upgradeFromV37ToV38),
DatabaseMigration(39, upgradeFromV38ToV39),
DatabaseMigration(40, upgradeFromV39ToV40),
DatabaseMigration(41, upgradeFromV40ToV41),
];
class DatabaseService {

View File

@ -1,10 +1,9 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV12ToV13(Database db) async {
await db.execute(
'''
CREATE TABLE $omemoFingerprintCache (
CREATE TABLE OmemoFingerprintCache (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
fingerprint TEXT NOT NULL,

View File

@ -0,0 +1,72 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV39ToV40(Database db) async {
// Remove the old tables
await db.execute('DROP TABLE OmemoDevices');
await db.execute('DROP TABLE OmemoDeviceList');
await db.execute('DROP TABLE OmemoTrustCacheList');
await db.execute('DROP TABLE OmemoTrustDeviceList');
await db.execute('DROP TABLE OmemoTrustEnableList');
await db.execute('DROP TABLE OmemoFingerprintCache');
// Create the new tables
await db.execute(
'''
CREATE TABLE $omemoDevicesTable (
jid TEXT NOT NULL PRIMARY KEY,
id INTEGER NOT NULL,
ikPub TEXT NOT NULL,
ik TEXT NOT NULL,
spkPub TEXT NOT NULL,
spk TEXT NOT NULL,
spkId INTEGER NOT NULL,
spkSig TEXT NOT NULL,
oldSpkPub TEXT,
oldSpk TEXT,
oldSpkId INTEGER,
opks TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceListTable (
jid TEXT NOT NULL PRIMARY KEY,
devices TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoRatchetsTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL,
dhsPub TEXT NOT NULL,
dhs TEXT NOT NULL,
dhrPub TEXT,
rk TEXT NOT NULL,
cks TEXT,
ckr TEXT,
ns INTEGER NOT NULL,
nr INTEGER NOT NULL,
pn INTEGER NOT NULL,
ik TEXT NOT NULL,
ad TEXT NOT NULL,
skipped TEXT NOT NULL,
kex TEXT NOT NULL,
acked INTEGER NOT NULL,
PRIMARY KEY (jid, device)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL,
trust INTEGER NOT NULL,
enabled INTEGER NOT NULL,
PRIMARY KEY (jid, device)
)''',
);
}

View File

@ -0,0 +1,25 @@
import 'dart:convert';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV40ToV41(Database db) async {
final messages = await db.query(
messagesTable,
where: 'pseudoMessageType IS NOT NULL',
);
for (final message in messages) {
await db.insert(
messagesTable,
{
...message,
'pseudoMessageData': jsonEncode({
'ratchetsAdded': 1,
'ratchetsReplaced': 0,
}),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}

View File

@ -776,7 +776,7 @@ Future<void> performGetOmemoFingerprints(
final omemo = GetIt.I.get<OmemoService>();
sendEvent(
GetConversationOmemoFingerprintsResult(
fingerprints: await omemo.getOmemoKeysForJid(command.jid),
fingerprints: await omemo.getFingerprintsForJid(command.jid),
),
id: id,
);
@ -789,7 +789,7 @@ Future<void> performEnableOmemoKey(
final id = extra as String;
final omemo = GetIt.I.get<OmemoService>();
await omemo.setOmemoKeyEnabled(
await omemo.setDeviceEnablement(
command.jid,
command.deviceId,
command.enabled,
@ -805,10 +805,14 @@ Future<void> performRecreateSessions(
RecreateSessionsCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<OmemoService>().removeAllSessions(command.jid);
// Remove all ratchets
await GetIt.I.get<OmemoService>().removeAllRatchets(command.jid);
final conn = GetIt.I.get<XmppConnection>();
await conn.getManagerById<BaseOmemoManager>(omemoManager)!.sendOmemoHeartbeat(
// And force the creation of new ones
await GetIt.I
.get<XmppConnection>()
.getManagerById<OmemoManager>(omemoManager)!
.sendOmemoHeartbeat(
command.jid,
);
}
@ -837,14 +841,14 @@ Future<void> performGetOwnOmemoFingerprints(
final id = extra as String;
final os = GetIt.I.get<OmemoService>();
final xs = GetIt.I.get<XmppService>();
await os.ensureInitialized();
final jid = (await xs.getConnectionSettings())!.jid;
final device = await os.getDevice();
sendEvent(
GetOwnOmemoFingerprintsResult(
ownDeviceFingerprint: await os.getDeviceFingerprint(),
ownDeviceId: await os.getDeviceId(),
fingerprints: await os.getOwnFingerprints(jid),
ownDeviceFingerprint: await device.getFingerprint(),
ownDeviceId: device.id,
fingerprints: await os.getFingerprintsForJid(jid.toString()),
),
id: id,
);
@ -856,7 +860,7 @@ Future<void> performRemoveOwnDevice(
}) async {
await GetIt.I
.get<XmppConnection>()
.getManagerById<BaseOmemoManager>(omemoManager)!
.getManagerById<OmemoManager>(omemoManager)!
.deleteDevice(command.deviceId);
}
@ -865,9 +869,7 @@ Future<void> performRegenerateOwnDevice(
dynamic extra,
}) async {
final id = extra as String;
final jid =
GetIt.I.get<XmppConnection>().connectionSettings.jid.toBare().toString();
final device = await GetIt.I.get<OmemoService>().regenerateDevice(jid);
final device = await GetIt.I.get<OmemoService>().regenerateDevice();
sendEvent(
RegenerateOwnDeviceResult(device: device),
@ -1041,9 +1043,9 @@ Future<void> performMarkDeviceVerified(
MarkOmemoDeviceAsVerifiedCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<OmemoService>().verifyDevice(
command.deviceId,
await GetIt.I.get<OmemoService>().setDeviceVerified(
command.jid,
command.deviceId,
);
}

View File

@ -329,7 +329,7 @@ FROM (SELECT * FROM $messagesTable WHERE $query ORDER BY timestamp DESC LIMIT $s
bool isDownloading = false,
bool isUploading = false,
String? stickerPackId,
int? pseudoMessageType,
PseudoMessageType? pseudoMessageType,
Map<String, dynamic>? pseudoMessageData,
bool received = false,
bool displayed = false,

View File

@ -1,49 +0,0 @@
import 'package:get_it/get_it.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/omemo/omemo.dart';
import 'package:omemo_dart/omemo_dart.dart';
class MoxxyOmemoManager extends BaseOmemoManager {
MoxxyOmemoManager() : super();
@override
Future<OmemoManager> getOmemoManager() async {
final os = GetIt.I.get<OmemoService>();
await os.ensureInitialized();
return os.omemoManager;
}
@override
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza) async {
// Never encrypt stanzas that contain PubSub elements
if (stanza.firstTag('pubsub', xmlns: pubsubXmlns) != null ||
stanza.firstTag('pubsub', xmlns: pubsubOwnerXmlns) != null ||
stanza.firstTagByXmlns(carbonsXmlns) != null ||
stanza.firstTagByXmlns(rosterXmlns) != null) {
return false;
}
// Encrypt when the conversation is set to use OMEMO.
return GetIt.I
.get<ConversationService>()
.shouldEncryptForConversation(toJid);
}
}
class MoxxyBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager {
MoxxyBTBVTrustManager(
Map<RatchetMapKey, BTBVTrustState> trustCache,
Map<RatchetMapKey, bool> enablementCache,
Map<String, List<int>> devices,
) : super(
trustCache: trustCache,
enablementCache: enablementCache,
devices: devices,
);
@override
Future<void> commitState() async {
await GetIt.I.get<OmemoService>().commitTrustManager(await toJson());
}
}

View File

@ -1,213 +1,43 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:hex/hex.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart' as moxxmpp;
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/message.dart';
import 'package:moxxyv2/service/moxxmpp/omemo.dart';
import 'package:moxxyv2/service/omemo/implementations.dart';
import 'package:moxxyv2/service/omemo/types.dart';
import 'package:moxxyv2/service/omemo/persistence.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/models/omemo_device.dart' as model;
import 'package:omemo_dart/omemo_dart.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:synchronized/synchronized.dart';
class OmemoDoubleRatchetWrapper {
OmemoDoubleRatchetWrapper(this.ratchet, this.id, this.jid);
final OmemoDoubleRatchet ratchet;
final int id;
final String jid;
}
class OmemoService {
/// Logger.
final Logger _log = Logger('OmemoService');
/// Flag indicating whether we are initialized.
bool _initialized = false;
/// Flag indicating whether the initialization is currently running.
bool _running = false;
/// Lock guarding access to [_waitingForInitialization], [_running], and [_initialized].
final Lock _lock = Lock();
/// Queue for code that is waiting on the service initialization.
final Queue<Completer<void>> _waitingForInitialization =
Queue<Completer<void>>();
final Map<String, Map<int, String>> _fingerprintCache = {};
late OmemoManager omemoManager;
/// The manager to use for OMEMO.
late OmemoManager _omemoManager;
Future<void> initializeIfNeeded(String jid) async {
final done = await _lock.synchronized(() => _initialized);
if (done) return;
final device = await _loadOmemoDevice(jid);
final ratchetMap = <RatchetMapKey, OmemoDoubleRatchet>{};
final deviceList = <String, List<int>>{};
if (device == null) {
_log.info('No OMEMO marker found. Generating OMEMO identity...');
} else {
_log.info('OMEMO marker found. Restoring OMEMO state...');
for (final ratchet in await _loadRatchets()) {
final key = RatchetMapKey(ratchet.jid, ratchet.id);
ratchetMap[key] = ratchet.ratchet;
}
deviceList.addAll(await _loadOmemoDeviceList());
}
final om = GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
omemoManager = OmemoManager(
device ?? await compute(generateNewIdentityImpl, jid),
await loadTrustManager(),
om.sendEmptyMessageImpl,
om.fetchDeviceList,
om.fetchDeviceBundle,
om.subscribeToDeviceListImpl,
);
if (device == null) {
await commitDevice(await omemoManager.getDevice());
await commitDeviceMap(<String, List<int>>{});
await commitTrustManager(await omemoManager.trustManager.toJson());
}
omemoManager.initialize(
ratchetMap,
deviceList,
);
omemoManager.eventStream.listen((event) async {
if (event is RatchetModifiedEvent) {
await _saveRatchet(
OmemoDoubleRatchetWrapper(
event.ratchet,
event.deviceId,
event.jid,
),
);
if (event.added) {
// Cache the fingerprint
final fingerprint = await event.ratchet.getOmemoFingerprint();
await _addFingerprintsToCache([
OmemoCacheTriple(
event.jid,
event.deviceId,
fingerprint,
),
]);
if (_fingerprintCache.containsKey(event.jid)) {
_fingerprintCache[event.jid]![event.deviceId] = fingerprint;
}
await addNewDeviceMessage(event.jid, event.deviceId);
}
} else if (event is DeviceListModifiedEvent) {
await commitDeviceMap(event.list);
} else if (event is DeviceModifiedEvent) {
await commitDevice(event.device);
// Publish it
await GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!
.publishBundle(await event.device.toBundle());
}
});
await _lock.synchronized(() {
_initialized = true;
for (final c in _waitingForInitialization) {
c.complete();
}
_waitingForInitialization.clear();
});
}
/// Adds a pseudo message saying that [jid] added a new device with id [deviceId].
/// If, however, [jid] is our own JID, then nothing is done.
Future<void> addNewDeviceMessage(String jid, int deviceId) async {
// Add a pseudo message if it is not about our own devices
final xmppState = await GetIt.I.get<XmppStateService>().getXmppState();
if (jid == xmppState.jid) return;
final ms = GetIt.I.get<MessageService>();
final message = await ms.addMessageFromData(
'',
DateTime.now().millisecondsSinceEpoch,
'',
jid,
'',
false,
false,
false,
pseudoMessageType: pseudoMessageTypeNewDevice,
pseudoMessageData: <String, dynamic>{
'deviceId': deviceId,
'jid': jid,
},
);
sendEvent(
MessageAddedEvent(
message: message,
),
);
}
Future<model.OmemoDevice> regenerateDevice(String jid) async {
// Prevent access to the session manager as it is (mostly) guarded ensureInitialized
await _lock.synchronized(() {
_initialized = false;
});
_log.info('No OMEMO marker found. Generating OMEMO identity...');
final oldId = await omemoManager.getDeviceId();
// Clear the database
await _emptyOmemoSessionTables();
// Regenerate the identity in the background
final device = await compute(generateNewIdentityImpl, jid);
await omemoManager.replaceDevice(device);
await commitDevice(device);
await commitDeviceMap(<String, List<int>>{});
await commitTrustManager(await omemoManager.trustManager.toJson());
// Remove the old device
final omemo = GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
await omemo.deleteDevice(oldId);
// Publish the new one
await omemo.publishBundle(await omemoManager.getDeviceBundle());
// Allow access again
await _lock.synchronized(() {
_initialized = true;
for (final c in _waitingForInitialization) {
c.complete();
}
_waitingForInitialization.clear();
});
// Return the OmemoDevice
return model.OmemoDevice(
await getDeviceFingerprint(),
true,
true,
true,
await getDeviceId(),
);
/// Access the underlying [OmemoManager].
Future<OmemoManager> getOmemoManager() async {
await ensureInitialized();
return _omemoManager;
}
/// Ensures that the code following this *AWAITED* call can access every method
@ -228,27 +58,79 @@ class OmemoService {
}
}
Future<void> commitDeviceMap(Map<String, List<int>> deviceMap) async {
await _saveOmemoDeviceList(deviceMap);
/// Creates or loads the [OmemoManager] for the JID [jid].
Future<void> initializeIfNeeded(String jid) async {
final done = await _lock.synchronized(() {
// Do nothing if we're already initialized
if (_initialized) {
return true;
}
// Lock the execution if we're not yet running.
if (_running) {
return true;
}
_running = true;
return false;
});
if (done) return;
final device = await loadOmemoDevice(jid);
if (device == null) {
_log.info('No OMEMO marker found. Generating OMEMO identity...');
} else {
_log.info('OMEMO marker found. Restoring OMEMO state...');
}
final om = GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!;
_omemoManager = OmemoManager(
device ?? await compute(generateNewIdentityImpl, jid),
BlindTrustBeforeVerificationTrustManager(
commit: commitTrust,
loadData: loadTrust,
removeTrust: removeTrust,
),
om.sendEmptyMessageImpl,
om.fetchDeviceList,
om.fetchDeviceBundle,
om.subscribeToDeviceListImpl,
om.publishDeviceImpl,
commitDevice: commitDevice,
commitRatchets: commitRatchets,
commitDeviceList: commitDeviceList,
removeRatchets: removeRatchets,
loadRatchets: loadRatchets,
);
if (device == null) {
await commitDevice(await _omemoManager.getDevice());
}
await _lock.synchronized(() {
_running = false;
_initialized = true;
for (final c in _waitingForInitialization) {
c.complete();
}
_waitingForInitialization.clear();
});
}
Future<void> commitDevice(OmemoDevice device) async {
await _saveOmemoDevice(device);
}
/// Requests our device list and checks if the current device is in it. If not, then
/// it will be published.
Future<Object?> publishDeviceIfNeeded() async {
Future<moxxmpp.OmemoError?> publishDeviceIfNeeded() async {
_log.finest('publishDeviceIfNeeded: Waiting for initialization...');
await ensureInitialized();
_log.finest('publishDeviceIfNeeded: Done');
final conn = GetIt.I.get<moxxmpp.XmppConnection>();
final omemo =
conn.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
conn.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!;
final dm = conn.getManagerById<moxxmpp.DiscoManager>(moxxmpp.discoManager)!;
final bareJid = conn.connectionSettings.jid.toBare();
final device = await omemoManager.getDevice();
final device = await _omemoManager.getDevice();
final bundlesRaw = await dm.discoItemsQuery(
bareJid,
@ -256,7 +138,7 @@ class OmemoService {
);
if (bundlesRaw.isType<moxxmpp.DiscoError>()) {
await omemo.publishBundle(await device.toBundle());
return bundlesRaw.get<moxxmpp.DiscoError>();
return null;
}
final bundleIds = bundlesRaw
@ -285,469 +167,114 @@ class OmemoService {
return null;
}
Future<void> _fetchFingerprintsAndCache(moxxmpp.JID jid) async {
final bareJid = jid.toBare().toString();
final allDevicesRaw = await GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!
.retrieveDeviceBundles(jid);
if (allDevicesRaw.isType<List<OmemoBundle>>()) {
final allDevices = allDevicesRaw.get<List<OmemoBundle>>();
final map = <int, String>{};
final items = List<OmemoCacheTriple>.empty(growable: true);
for (final device in allDevices) {
final curveIk = await device.ik.toCurve25519();
final fingerprint = HEX.encode(await curveIk.getBytes());
map[device.id] = fingerprint;
items.add(OmemoCacheTriple(bareJid, device.id, fingerprint));
}
// Cache them in memory
_fingerprintCache[bareJid] = map;
// Cache them in the database
await _addFingerprintsToCache(items);
}
}
Future<void> _loadOrFetchFingerprints(moxxmpp.JID jid) async {
final bareJid = jid.toBare().toString();
if (!_fingerprintCache.containsKey(bareJid)) {
// First try to load it from the database
final triples = await _getFingerprintsFromCache(bareJid);
if (triples.isEmpty) {
// We found no fingerprints in the database, so try to fetch them
await _fetchFingerprintsAndCache(jid);
} else {
// We have fetched fingerprints from the database
_fingerprintCache[bareJid] = Map<int, String>.fromEntries(
triples.map((triple) {
return MapEntry<int, String>(
triple.deviceId,
triple.fingerprint,
);
}),
);
}
}
}
Future<List<model.OmemoDevice>> getOmemoKeysForJid(String jid) async {
Future<void> onNewConnection() async {
await ensureInitialized();
// Get finger prints if we have to
await _loadOrFetchFingerprints(moxxmpp.JID.fromString(jid));
final keys = List<model.OmemoDevice>.empty(growable: true);
final tm =
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
final trustMap = await tm.getDevicesTrust(jid);
if (!_fingerprintCache.containsKey(jid)) return [];
for (final deviceId in _fingerprintCache[jid]!.keys) {
keys.add(
model.OmemoDevice(
_fingerprintCache[jid]![deviceId]!,
await tm.isTrusted(jid, deviceId),
trustMap[deviceId] == BTBVTrustState.verified,
await tm.isEnabled(jid, deviceId),
deviceId,
),
);
}
return keys;
await _omemoManager.onNewConnection();
}
Future<void> commitTrustManager(Map<String, dynamic> json) async {
await _saveTrustCache(
json['trust']! as Map<String, int>,
);
await _saveTrustEnablementList(
json['enable']! as Map<String, bool>,
);
await _saveTrustDeviceList(
json['devices']! as Map<String, List<int>>,
);
}
Future<MoxxyBTBVTrustManager> loadTrustManager() async {
return MoxxyBTBVTrustManager(
await _loadTrustCache(),
await _loadTrustEnablementList(),
await _loadTrustDeviceList(),
);
}
Future<void> setOmemoKeyEnabled(
String jid,
int deviceId,
bool enabled,
) async {
Future<List<model.OmemoDevice>> getFingerprintsForJid(String jid) async {
await ensureInitialized();
await omemoManager.trustManager.setEnabled(jid, deviceId, enabled);
}
final fingerprints = await _omemoManager.getFingerprintsForJid(jid) ?? [];
var trust = <int, BTBVTrustData>{};
Future<void> removeAllSessions(String jid) async {
await ensureInitialized();
await omemoManager.removeAllRatchets(jid);
}
Future<int> getDeviceId() async {
await ensureInitialized();
return omemoManager.getDeviceId();
}
Future<String> getDeviceFingerprint() => omemoManager.getDeviceFingerprint();
/// Returns a list of OmemoDevices for devices we have sessions with and other devices
/// published on [ownJid]'s devices PubSub node.
/// Note that the list is made so that the current device is excluded.
Future<List<model.OmemoDevice>> getOwnFingerprints(moxxmpp.JID ownJid) async {
final ownId = await getDeviceId();
final keys = List<model.OmemoDevice>.from(
await getOmemoKeysForJid(ownJid.toString()),
);
final bareJid = ownJid.toBare().toString();
// Get fingerprints if we have to
await _loadOrFetchFingerprints(ownJid);
final tm =
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
final trustMap = await tm.getDevicesTrust(bareJid);
for (final deviceId in _fingerprintCache[bareJid]!.keys) {
if (deviceId == ownId) continue;
if (keys.indexWhere((key) => key.deviceId == deviceId) != -1) continue;
final fingerprint = _fingerprintCache[bareJid]![deviceId]!;
keys.add(
model.OmemoDevice(
fingerprint,
await tm.isTrusted(bareJid, deviceId),
trustMap[deviceId] == BTBVTrustState.verified,
await tm.isEnabled(bareJid, deviceId),
deviceId,
hasSessionWith: false,
),
);
}
return keys;
}
Future<void> verifyDevice(int deviceId, String jid) async {
final tm =
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
await tm.setDeviceTrust(
await _omemoManager.withTrustManager(
jid,
deviceId,
BTBVTrustState.verified,
(tm) async {
trust = await (tm as BlindTrustBeforeVerificationTrustManager)
.getDevicesTrust(jid);
},
);
}
/// Tells omemo_dart, that certain caches are to be seen as invalidated.
void onNewConnection() {
if (_initialized) {
omemoManager.onNewConnection();
}
}
/// Database methods
Future<List<OmemoDoubleRatchetWrapper>> _loadRatchets() async {
final results =
await GetIt.I.get<DatabaseService>().database.query(omemoRatchetsTable);
return results.map((ratchet) {
final json = jsonDecode(ratchet['mkskipped']! as String) as List<dynamic>;
final mkskipped = List<Map<String, dynamic>>.empty(growable: true);
for (final i in json) {
final element = i as Map<String, dynamic>;
mkskipped.add({
'key': element['key']! as String,
'public': element['public']! as String,
'n': element['n']! as int,
});
}
return OmemoDoubleRatchetWrapper(
OmemoDoubleRatchet.fromJson(
{
...ratchet,
'acknowledged': intToBool(ratchet['acknowledged']! as int),
'mkskipped': mkskipped,
},
),
ratchet['id']! as int,
ratchet['jid']! as String,
return fingerprints.map((fp) {
return model.OmemoDevice(
fp.fingerprint,
trust[fp.deviceId]?.trusted ?? false,
trust[fp.deviceId]?.state == BTBVTrustState.verified,
trust[fp.deviceId]?.enabled ?? false,
fp.deviceId,
);
}).toList();
}
Future<void> _saveRatchet(OmemoDoubleRatchetWrapper ratchet) async {
final json = await ratchet.ratchet.toJson();
await GetIt.I.get<DatabaseService>().database.insert(
omemoRatchetsTable,
{
...json,
'mkskipped': jsonEncode(json['mkskipped']),
'acknowledged': boolToInt(json['acknowledged']! as bool),
'jid': ratchet.jid,
'id': ratchet.id,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<Map<RatchetMapKey, BTBVTrustState>> _loadTrustCache() async {
final entries = await GetIt.I
.get<DatabaseService>()
.database
.query(omemoTrustCacheTable);
final mapEntries =
entries.map<MapEntry<RatchetMapKey, BTBVTrustState>>((entry) {
// TODO(PapaTutuWawa): Expose this from omemo_dart
BTBVTrustState state;
final value = entry['trust']! as int;
if (value == 1) {
state = BTBVTrustState.notTrusted;
} else if (value == 2) {
state = BTBVTrustState.blindTrust;
} else if (value == 3) {
state = BTBVTrustState.verified;
} else {
state = BTBVTrustState.notTrusted;
}
return MapEntry(
RatchetMapKey.fromJsonKey(entry['key']! as String),
state,
);
Future<void> setDeviceEnablement(String jid, int device, bool state) async {
await ensureInitialized();
await _omemoManager.withTrustManager(jid, (tm) async {
await (tm as BlindTrustBeforeVerificationTrustManager)
.setEnabled(jid, device, state);
});
return Map.fromEntries(mapEntries);
}
Future<void> _saveTrustCache(Map<String, int> cache) async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
// ignore: cascade_invocations
batch.delete(omemoTrustCacheTable);
for (final entry in cache.entries) {
batch.insert(
omemoTrustCacheTable,
{
'key': entry.key,
'trust': entry.value,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit();
}
Future<Map<RatchetMapKey, bool>> _loadTrustEnablementList() async {
final entries = await GetIt.I
.get<DatabaseService>()
.database
.query(omemoTrustEnableListTable);
final mapEntries = entries.map<MapEntry<RatchetMapKey, bool>>((entry) {
return MapEntry(
RatchetMapKey.fromJsonKey(entry['key']! as String),
intToBool(entry['enabled']! as int),
);
Future<void> setDeviceVerified(String jid, int device) async {
await ensureInitialized();
await _omemoManager.withTrustManager(jid, (tm) async {
await (tm as BlindTrustBeforeVerificationTrustManager)
.setDeviceTrust(jid, device, BTBVTrustState.verified);
});
return Map.fromEntries(mapEntries);
}
Future<void> _saveTrustEnablementList(Map<String, bool> list) async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
// ignore: cascade_invocations
batch.delete(omemoTrustEnableListTable);
for (final entry in list.entries) {
batch.insert(
omemoTrustEnableListTable,
{
'key': entry.key,
'enabled': boolToInt(entry.value),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit();
Future<void> removeAllRatchets(String jid) async {
await ensureInitialized();
await _omemoManager.removeAllRatchets(jid);
}
Future<Map<String, List<int>>> _loadTrustDeviceList() async {
final entries = await GetIt.I
.get<DatabaseService>()
.database
.query(omemoTrustDeviceListTable);
final map = <String, List<int>>{};
for (final entry in entries) {
final key = entry['jid']! as String;
final device = entry['device']! as int;
if (map.containsKey(key)) {
map[key]!.add(device);
} else {
map[key] = [device];
}
}
return map;
Future<OmemoDevice> getDevice() async {
await ensureInitialized();
return _omemoManager.getDevice();
}
Future<void> _saveTrustDeviceList(Map<String, List<int>> list) async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
Future<model.OmemoDevice> regenerateDevice() async {
await ensureInitialized();
// ignore: cascade_invocations
batch.delete(omemoTrustDeviceListTable);
for (final entry in list.entries) {
for (final device in entry.value) {
batch.insert(
omemoTrustDeviceListTable,
{
'jid': entry.key,
'device': device,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
final oldDeviceId = (await getDevice()).id;
await batch.commit();
}
// Generate the new device
final newDevice = await _omemoManager.regenerateDevice();
Future<void> _saveOmemoDevice(OmemoDevice device) async {
await GetIt.I.get<DatabaseService>().database.insert(
omemoDeviceTable,
{
'jid': device.jid,
'id': device.id,
'data': jsonEncode(await device.toJson()),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<OmemoDevice?> _loadOmemoDevice(String jid) async {
final data = await GetIt.I.get<DatabaseService>().database.query(
omemoDeviceTable,
where: 'jid = ?',
whereArgs: [jid],
limit: 1,
);
if (data.isEmpty) return null;
final deviceJson =
jsonDecode(data.first['data']! as String) as Map<String, dynamic>;
// NOTE: We need to do this because Dart otherwise complains about not being able
// to cast dynamic to List<int>.
final opks = List<Map<String, dynamic>>.empty(growable: true);
final opksIter = deviceJson['opks']! as List<dynamic>;
for (final tmpOpk in opksIter) {
final opk = tmpOpk as Map<String, dynamic>;
opks.add(<String, dynamic>{
'id': opk['id']! as int,
'public': opk['public']! as String,
'private': opk['private']! as String,
});
}
deviceJson['opks'] = opks;
return OmemoDevice.fromJson(deviceJson);
}
Future<Map<String, List<int>>> _loadOmemoDeviceList() async {
final list = await GetIt.I
.get<DatabaseService>()
.database
.query(omemoDeviceListTable);
final map = <String, List<int>>{};
for (final entry in list) {
final key = entry['jid']! as String;
final id = entry['id']! as int;
if (map.containsKey(key)) {
map[key]!.add(id);
} else {
map[key] = [id];
}
}
return map;
}
Future<void> _saveOmemoDeviceList(Map<String, List<int>> list) async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
// ignore: cascade_invocations
batch.delete(omemoDeviceListTable);
for (final entry in list.entries) {
for (final id in entry.value) {
batch.insert(
omemoDeviceListTable,
{
'jid': entry.key,
'id': id,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
await batch.commit();
}
Future<void> _emptyOmemoSessionTables() async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
// ignore: cascade_invocations
batch
..delete(omemoRatchetsTable)
..delete(omemoTrustCacheTable)
..delete(omemoTrustEnableListTable);
await batch.commit();
}
Future<void> _addFingerprintsToCache(List<OmemoCacheTriple> items) async {
final batch = GetIt.I.get<DatabaseService>().database.batch();
for (final item in items) {
batch.insert(
omemoFingerprintCache,
<String, dynamic>{
'jid': item.jid,
'id': item.deviceId,
'fingerprint': item.fingerprint,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit();
}
Future<List<OmemoCacheTriple>> _getFingerprintsFromCache(String jid) async {
final rawItems = await GetIt.I.get<DatabaseService>().database.query(
omemoFingerprintCache,
where: 'jid = ?',
whereArgs: [jid],
// Remove the old device
unawaited(
GetIt.I
.get<moxxmpp.XmppConnection>()
.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!
.deleteDevice(oldDeviceId),
);
return rawItems.map((item) {
return OmemoCacheTriple(
jid,
item['id']! as int,
item['fingerprint']! as String,
);
}).toList();
return model.OmemoDevice(
await newDevice.getFingerprint(),
true,
true,
true,
newDevice.id,
);
}
/// Adds a pseudo-message of type [type] to the chat with [conversationJid].
/// Also sends an event to the UI.
Future<void> addPseudoMessage(
String conversationJid,
PseudoMessageType type,
int ratchetsAdded,
int ratchetsReplaced,
) async {
final ms = GetIt.I.get<MessageService>();
final message = await ms.addMessageFromData(
'',
DateTime.now().millisecondsSinceEpoch,
'',
conversationJid,
'',
false,
false,
false,
pseudoMessageType: type,
pseudoMessageData: {
'ratchetsAdded': ratchetsAdded,
'ratchetsReplaced': ratchetsReplaced,
},
);
sendEvent(
MessageAddedEvent(
message: message,
),
);
}
}

View File

@ -0,0 +1,308 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:sqflite_common/sql.dart';
extension ByteListHelpers on List<int> {
String toBase64() {
return base64Encode(this);
}
OmemoPublicKey toPublicKey(KeyPairType type) {
return OmemoPublicKey.fromBytes(this, type);
}
}
Future<void> commitDevice(OmemoDevice device) async {
final db = GetIt.I.get<DatabaseService>().database;
final serializedOpks = <String, Map<String, String>>{};
for (final entry in device.opks.entries) {
serializedOpks[entry.key.toString()] = {
'public': base64Encode(await entry.value.pk.getBytes()),
'private': base64Encode(await entry.value.sk.getBytes()),
};
}
await db.insert(
omemoDevicesTable,
{
'jid': device.jid,
'id': device.id,
'ikPub': base64Encode(await device.ik.pk.getBytes()),
'ik': base64Encode(await device.ik.sk.getBytes()),
'spkPub': base64Encode(await device.spk.pk.getBytes()),
'spk': base64Encode(await device.spk.sk.getBytes()),
'spkId': device.spkId,
'spkSig': base64Encode(device.spkSignature),
'oldSpkPub': (await device.oldSpk?.pk.getBytes())?.toBase64(),
'oldSpk': (await device.oldSpk?.sk.getBytes())?.toBase64(),
'oldSpkId': device.oldSpkId,
'opks': jsonEncode(serializedOpks),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<OmemoDevice?> loadOmemoDevice(String jid) async {
final db = GetIt.I.get<DatabaseService>().database;
final rawDevice = await db.query(
omemoDevicesTable,
where: 'jid = ?',
whereArgs: [jid],
limit: 1,
);
if (rawDevice.isEmpty) {
return null;
}
final deviceJson = rawDevice.first;
// Deserialize the OPKs first
final deserializedOpks = <int, OmemoKeyPair>{};
final opks =
(jsonDecode(rawDevice.first['opks']! as String) as Map<dynamic, dynamic>)
.cast<String, dynamic>();
for (final opk in opks.entries) {
final opkValue = (opk.value as Map<String, dynamic>).cast<String, String>();
deserializedOpks[int.parse(opk.key)] = OmemoKeyPair.fromBytes(
base64Decode(opkValue['public']!),
base64Decode(opkValue['private']!),
KeyPairType.x25519,
);
}
OmemoKeyPair? oldSpk;
if (deviceJson['oldSpkPub'] != null && deviceJson['oldSpk'] != null) {
oldSpk = OmemoKeyPair.fromBytes(
base64Decode(deviceJson['oldSpkPub']! as String),
base64Decode(deviceJson['oldSpk']! as String),
KeyPairType.x25519,
);
}
return OmemoDevice(
jid,
deviceJson['id']! as int,
OmemoKeyPair.fromBytes(
base64Decode(deviceJson['ikPub']! as String),
base64Decode(deviceJson['ik']! as String),
KeyPairType.ed25519,
),
OmemoKeyPair.fromBytes(
base64Decode(deviceJson['spkPub']! as String),
base64Decode(deviceJson['spk']! as String),
KeyPairType.x25519,
),
deviceJson['spkId']! as int,
base64Decode(deviceJson['spkSig']! as String),
oldSpk,
deviceJson['oldSpkId'] as int?,
deserializedOpks,
);
}
Future<void> commitRatchets(List<OmemoRatchetData> ratchets) async {
final db = GetIt.I.get<DatabaseService>().database;
final batch = db.batch();
for (final ratchet in ratchets) {
// Serialize the skipped keys
final serializedSkippedKeys = <Map<String, Object>>[];
for (final sk in ratchet.ratchet.mkSkipped.entries) {
serializedSkippedKeys.add({
'dhPub': (await sk.key.dh.getBytes()).toBase64(),
'n': sk.key.n,
'mk': sk.value.toBase64(),
});
}
// Serialize the KEX
final kex = {
'pkId': ratchet.ratchet.kex.pkId,
'spkId': ratchet.ratchet.kex.spkId,
'ek': (await ratchet.ratchet.kex.ek.getBytes()).toBase64(),
'ik': (await ratchet.ratchet.kex.ik.getBytes()).toBase64(),
};
batch.insert(
omemoRatchetsTable,
{
'jid': ratchet.jid,
'device': ratchet.id,
'dhsPub': base64Encode(await ratchet.ratchet.dhs.pk.getBytes()),
'dhs': base64Encode(await ratchet.ratchet.dhs.sk.getBytes()),
'dhrPub': (await ratchet.ratchet.dhr?.getBytes())?.toBase64(),
'rk': base64Encode(ratchet.ratchet.rk),
'cks': ratchet.ratchet.cks?.toBase64(),
'ckr': ratchet.ratchet.ckr?.toBase64(),
'ns': ratchet.ratchet.ns,
'nr': ratchet.ratchet.nr,
'pn': ratchet.ratchet.pn,
'ik': (await ratchet.ratchet.ik.getBytes()).toBase64(),
'ad': ratchet.ratchet.sessionAd.toBase64(),
'skipped': jsonEncode(serializedSkippedKeys),
'kex': jsonEncode(kex),
'acked': boolToInt(ratchet.ratchet.acknowledged),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit();
}
Future<void> commitDeviceList(String jid, List<int> devices) async {
final db = GetIt.I.get<DatabaseService>().database;
await db.insert(
omemoDeviceListTable,
{
'jid': jid,
'devices': jsonEncode(devices),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> removeRatchets(List<RatchetMapKey> ratchets) async {
final db = GetIt.I.get<DatabaseService>().database;
final batch = db.batch();
for (final key in ratchets) {
batch.delete(
omemoRatchetsTable,
where: 'jid = ? AND device = ?',
whereArgs: [key.jid, key.deviceId],
);
}
await batch.commit();
}
Future<OmemoDataPackage?> loadRatchets(String jid) async {
final db = GetIt.I.get<DatabaseService>().database;
final ratchetsRaw = await db.query(
omemoRatchetsTable,
where: 'jid = ?',
whereArgs: [jid],
);
final deviceListRaw = await db.query(
omemoDeviceListTable,
where: 'jid = ?',
whereArgs: [jid],
limit: 1,
);
if (ratchetsRaw.isEmpty || deviceListRaw.isEmpty) {
return null;
}
// Deserialize the ratchets
final ratchets = <RatchetMapKey, OmemoDoubleRatchet>{};
for (final ratchetRaw in ratchetsRaw) {
final key = RatchetMapKey(
jid,
ratchetRaw['device']! as int,
);
// Deserialize skipped keys
final mkSkipped = <SkippedKey, List<int>>{};
final skippedKeysRaw =
(jsonDecode(ratchetRaw['skipped']! as String) as List<dynamic>)
.cast<Map<dynamic, dynamic>>();
for (final skippedRaw in skippedKeysRaw) {
final key = SkippedKey(
(skippedRaw['dhPub']! as String)
.fromBase64()
.toPublicKey(KeyPairType.x25519),
skippedRaw['n']! as int,
);
mkSkipped[key] = (skippedRaw['mk']! as String).fromBase64();
}
// Deserialize the KEX
final kexRaw =
(jsonDecode(ratchetRaw['kex']! as String) as Map<dynamic, dynamic>)
.cast<String, Object>();
final kex = KeyExchangeData(
kexRaw['pkId']! as int,
kexRaw['spkId']! as int,
(kexRaw['ek']! as String).fromBase64().toPublicKey(KeyPairType.x25519),
(kexRaw['ik']! as String).fromBase64().toPublicKey(KeyPairType.ed25519),
);
// Deserialize the entire ratchet
ratchets[key] = OmemoDoubleRatchet(
OmemoKeyPair.fromBytes(
base64Decode(ratchetRaw['dhsPub']! as String),
base64Decode(ratchetRaw['dhs']! as String),
KeyPairType.x25519,
),
(ratchetRaw['dhrPub'] as String?)
?.fromBase64()
.toPublicKey(KeyPairType.x25519),
base64Decode(ratchetRaw['rk']! as String),
(ratchetRaw['cks'] as String?)?.fromBase64(),
(ratchetRaw['ckr'] as String?)?.fromBase64(),
ratchetRaw['ns']! as int,
ratchetRaw['nr']! as int,
ratchetRaw['pn']! as int,
(ratchetRaw['ik']! as String)
.fromBase64()
.toPublicKey(KeyPairType.ed25519),
(ratchetRaw['ad']! as String).fromBase64(),
mkSkipped,
intToBool(ratchetRaw['acked']! as int),
kex,
);
}
return OmemoDataPackage(
(jsonDecode(deviceListRaw.first['devices']! as String) as List<dynamic>)
.cast<int>(),
ratchets,
);
}
Future<void> commitTrust(BTBVTrustData trust) async {
final db = GetIt.I.get<DatabaseService>().database;
await db.insert(
omemoTrustTable,
{
'jid': trust.jid,
'device': trust.device,
'trust': trust.state.value,
'enabled': boolToInt(trust.enabled),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<BTBVTrustData>> loadTrust(String jid) async {
final db = GetIt.I.get<DatabaseService>().database;
final rawTrust = await db.query(
omemoTrustTable,
where: 'jid = ?',
whereArgs: [jid],
);
return rawTrust.map((trust) {
return BTBVTrustData(
jid,
trust['device']! as int,
BTBVTrustState.fromInt(trust['trust']! as int),
intToBool(trust['enabled']! as int),
false,
);
}).toList();
}
Future<void> removeTrust(String jid) async {
final db = GetIt.I.get<DatabaseService>().database;
await db.delete(
omemoTrustTable,
where: 'jid = ?',
whereArgs: [jid],
);
}

View File

@ -23,7 +23,6 @@ import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
import 'package:moxxyv2/service/language.dart';
import 'package:moxxyv2/service/message.dart';
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
import 'package:moxxyv2/service/moxxmpp/omemo.dart';
import 'package:moxxyv2/service/moxxmpp/roster.dart';
import 'package:moxxyv2/service/moxxmpp/socket.dart';
import 'package:moxxyv2/service/moxxmpp/stream.dart';
@ -222,7 +221,12 @@ Future<void> entrypoint() async {
const Identity(category: 'client', type: 'phone', name: 'Moxxy'),
]),
RosterManager(MoxxyRosterStateManager()),
MoxxyOmemoManager(),
OmemoManager(
GetIt.I.get<OmemoService>().getOmemoManager,
(toJid, _) async => GetIt.I
.get<ConversationService>()
.shouldEncryptForConversation(toJid),
),
PingManager(const Duration(minutes: 3)),
MessageManager(),
PresenceManager(),

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
@ -313,8 +314,7 @@ class XmppService {
// },
// );
final hasUrlSource = firstWhereOrNull(
sfs!.sources,
final hasUrlSource = sfs!.sources.firstWhereOrNull(
(src) => src is StatelessFileSharingUrlSource,
) !=
null;
@ -336,8 +336,7 @@ class XmppService {
sfs.metadata.size,
);
} else {
final encryptedSource = firstWhereOrNull(
sfs.sources,
final encryptedSource = sfs.sources.firstWhereOrNull(
(src) => src is StatelessFileSharingEncryptedSource,
)! as StatelessFileSharingEncryptedSource;
@ -387,22 +386,24 @@ class XmppService {
final manager = GetIt.I
.get<XmppConnection>()
.getManagerById<MessageManager>(messageManager)!;
if (isMarkable && info.features.contains(chatMarkersXmlns)) {
final hasId = originId != null || event.id != null;
if (isMarkable && info.features.contains(chatMarkersXmlns) && hasId) {
await manager.sendMessage(
event.from.toBare(),
TypedMap<StanzaHandlerExtension>.fromList([
ChatMarkerData(
ChatMarker.received,
originId ?? event.id,
originId ?? event.id!,
)
]),
);
} else if (deliveryReceiptRequested &&
info.features.contains(deliveryXmlns)) {
info.features.contains(deliveryXmlns) &&
hasId) {
await manager.sendMessage(
event.from.toBare(),
TypedMap<StanzaHandlerExtension>.fromList([
MessageDeliveryReceivedData(originId ?? event.id),
MessageDeliveryReceivedData(originId ?? event.id!),
]),
);
}
@ -780,7 +781,9 @@ class XmppService {
GetIt.I.get<BlocklistService>().onNewConnection();
// Reset the OMEMO cache
GetIt.I.get<OmemoService>().onNewConnection();
unawaited(
GetIt.I.get<OmemoService>().onNewConnection(),
);
// Enable carbons, if they're not already enabled (e.g. by using SASL2)
final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!;
@ -1054,10 +1057,17 @@ class XmppService {
return;
}
if (event.id == null) {
_log.warning(
'Received error message without id.',
);
return;
}
final ms = GetIt.I.get<MessageService>();
final msg = await ms.getMessageByStanzaId(
event.from.toBare().toString(),
event.id,
event.id!,
);
if (msg == null) {
@ -1214,7 +1224,7 @@ class XmppService {
// Stop the processing here if the event does not describe a displayable message
if (!_isMessageEventMessage(event) && event.encryptionError == null) return;
if (event.encryptionError is InvalidKeyExchangeException) return;
if (event.encryptionError is InvalidKeyExchangeSignatureError) return;
// Ignore File Upload Notifications where we don't have a filename.
final fun = event.extensions.get<FileUploadNotificationData>();
@ -1234,8 +1244,6 @@ class XmppService {
final isInRoster = rosterItem != null;
// True if the message was sent by us (via a Carbon)
final sent = isCarbon && event.from.toBare().toString() == state.jid;
// The timestamp at which we received the message
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
// Acknowledge the message if enabled
final receiptRequested =
@ -1294,14 +1302,60 @@ class XmppService {
);
}
// Log encryption errors
if (event.encryptionError != null) {
_log.warning(
'Got encryption error from moxxmpp for message: ${event.encryptionError}',
);
}
// Check if we have to create pseudo-messages related to OMEMO
final omemoData = event.get<OmemoData>();
if (omemoData != null) {
final addedRatchetsList =
omemoData.newRatchets.values.map((ids) => ids.length);
final amountAdded = addedRatchetsList.isEmpty
? 0
: addedRatchetsList.reduce((value, element) => value + element);
final replacedRatchetsList =
omemoData.replacedRatchets.values.map((ids) => ids.length);
final amountReplaced = replacedRatchetsList.isEmpty
? 0
: replacedRatchetsList.reduce((value, element) => value + element);
// Notify of new ratchets
final om = GetIt.I.get<OmemoService>();
if (omemoData.newRatchets.isNotEmpty) {
await om.addPseudoMessage(
conversationJid,
PseudoMessageType.newDevice,
amountAdded,
amountReplaced,
);
}
// Notify of changed ratchets
if (omemoData.replacedRatchets.isNotEmpty) {
await om.addPseudoMessage(
conversationJid,
PseudoMessageType.changedDevice,
amountAdded,
amountReplaced,
);
}
}
// Create the message in the database
// The timestamp at which we received the message
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
final ms = GetIt.I.get<MessageService>();
var message = await ms.addMessageFromData(
messageBody,
messageTimestamp,
event.from.toString(),
conversationJid,
event.id,
// TODO(Unknown): Should we handle this differently?
event.id ?? '',
fun != null,
event.encrypted,
event.extensions

View File

@ -1,4 +1,4 @@
import 'package:moxlib/awaitabledatasender.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/models/preferences.dart';

View File

@ -25,11 +25,9 @@ int errorTypeFromException(dynamic exception) {
return noError;
}
if (exception is NoDecryptionKeyException) {
return messageNoDecryptionKey;
} else if (exception is InvalidMessageHMACException) {
if (exception is InvalidMessageHMACError) {
return messageInvalidHMAC;
} else if (exception is NotEncryptedForDeviceException) {
} else if (exception is NotEncryptedForDeviceError) {
return messageNoDecryptionKey;
} else if (exception is InvalidAffixElementsException) {
return messageInvalidAffixElements;

View File

@ -1,4 +1,4 @@
import 'package:moxlib/awaitabledatasender.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/message.dart';

View File

@ -10,7 +10,30 @@ import 'package:moxxyv2/shared/warning_types.dart';
part 'message.freezed.dart';
part 'message.g.dart';
const pseudoMessageTypeNewDevice = 1;
extension PseudoMessageTypeFromInt on int {
PseudoMessageType? toPseudoMessageType() {
switch (this) {
case 1:
return PseudoMessageType.newDevice;
case 2:
return PseudoMessageType.changedDevice;
default:
return null;
}
}
}
enum PseudoMessageType {
/// Indicates that a new device was created in the chat.
newDevice(1),
/// Indicates that an existing device has been replaced.
changedDevice(2);
const PseudoMessageType(this.id);
final int id;
}
Map<String, dynamic> _optionalJsonDecodeWithFallback(String? data) {
if (data == null) return <String, dynamic>{};
@ -53,7 +76,7 @@ class Message with _$Message {
Message? quotes,
@Default([]) List<String> reactionsPreview,
String? stickerPackId,
int? pseudoMessageType,
PseudoMessageType? pseudoMessageType,
Map<String, dynamic>? pseudoMessageData,
}) = _Message;
@ -83,6 +106,13 @@ class Message with _$Message {
'isEdited': intToBool(json['isEdited']! as int),
'containsNoStore': intToBool(json['containsNoStore']! as int),
'reactionsPreview': reactionsPreview,
// NOTE: freezed expects the field name of the enum value here and refused to accept
// actual enum value. Makes sense since we have to serialize it, I guess.
'pseudoMessageType': (json['pseudoMessageType'] as int?)
?.toPseudoMessageType()
.toString()
.split('.')
.last,
'pseudoMessageData':
_optionalJsonDecodeWithFallback(json['pseudoMessageData'] as String?)
}).copyWith(
@ -114,6 +144,7 @@ class Message with _$Message {
'isRetracted': boolToInt(isRetracted),
'isEdited': boolToInt(isEdited),
'containsNoStore': boolToInt(containsNoStore),
'pseudoMessageType': pseudoMessageType?.id,
'pseudoMessageData': _optionalJsonEncodeWithFallback(pseudoMessageData),
};
}

View File

@ -11,9 +11,8 @@ class OmemoDevice with _$OmemoDevice {
bool trusted,
bool verified,
bool enabled,
int deviceId, {
@Default(true) bool hasSessionWith,
}) = _OmemoDevice;
int deviceId,
) = _OmemoDevice;
/// JSON
factory OmemoDevice.fromJson(Map<String, dynamic> json) =>

View File

@ -5,13 +5,27 @@ import 'package:moxxmpp/moxxmpp.dart';
part 'xmpp_state.freezed.dart';
part 'xmpp_state.g.dart';
extension StreamManagementStateToJson on StreamManagementState {
Map<String, dynamic> toJson() => {
'c2s': c2s,
's2c': s2c,
'streamResumptionLocation': streamResumptionLocation,
'streamResumptionId': streamResumptionId,
};
}
class StreamManagementStateConverter
implements JsonConverter<StreamManagementState, Map<String, dynamic>> {
const StreamManagementStateConverter();
@override
StreamManagementState fromJson(Map<String, dynamic> json) =>
StreamManagementState.fromJson(json);
StreamManagementState(
json['c2s']! as int,
json['s2c']! as int,
streamResumptionLocation: json['streamResumptionLocation'] as String?,
streamResumptionId: json['streamResumptionId'] as String?,
);
@override
Map<String, dynamic> toJson(StreamManagementState state) => state.toJson();

View File

@ -1,9 +1,9 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
@ -47,8 +47,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
) async {
final cb = GetIt.I.get<ConversationsBloc>();
await cb.waitUntilInitialized();
final conversation = firstWhereOrNull(
cb.state.conversations,
final conversation = cb.state.conversations.firstWhereOrNull(
(Conversation c) => c.jid == event.jid,
)!;
emit(

View File

@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/commands.dart';
@ -43,10 +43,11 @@ class NewConversationBloc
final conversations = GetIt.I.get<ConversationsBloc>();
// Guard against an unneccessary roundtrip
if (listContains(
conversations.state.conversations,
(Conversation c) => c.jid == event.jid,
)) {
final listContains = conversations.state.conversations.firstWhereOrNull(
(Conversation c) => c.jid == event.jid,
) !=
null;
if (listContains) {
GetIt.I.get<conversation.ConversationBloc>().add(
conversation.RequestedConversationEvent(
event.jid,
@ -120,8 +121,7 @@ class NewConversationBloc
if (event.removed.contains(item.jid)) continue;
// Handle modified items
final modified = firstWhereOrNull(
event.modified,
final modified = event.modified.firstWhereOrNull(
(RosterItem i) => i.id == item.id,
);
if (modified != null) {

View File

@ -87,17 +87,6 @@ class OwnDevicesBloc extends Bloc<OwnDevicesEvent, OwnDevicesState> {
RecreateSessionsCommand(jid: GetIt.I.get<UIDataService>().ownJid!),
awaitable: false,
);
emit(
state.copyWith(
keys: List.from(
state.keys.map(
(key) => key.copyWith(
hasSessionWith: false,
),
),
),
),
);
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
}

View File

@ -1,8 +1,8 @@
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/commands.dart';
@ -44,10 +44,13 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
);
// Apply
final stickerPack = firstWhereOrNull(
GetIt.I.get<stickers.StickersBloc>().state.stickerPacks,
(StickerPack pack) => pack.id == event.stickerPackId,
);
final stickerPack = GetIt.I
.get<stickers.StickersBloc>()
.state
.stickerPacks
.firstWhereOrNull(
(StickerPack pack) => pack.id == event.stickerPackId,
);
assert(stickerPack != null, 'The sticker pack must be found');
emit(
state.copyWith(
@ -177,10 +180,13 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
Emitter<StickerPackState> emit,
) async {
// Find out if the sticker pack is locally available or not
final stickerPack = firstWhereOrNull(
GetIt.I.get<stickers.StickersBloc>().state.stickerPacks,
(StickerPack pack) => pack.id == event.stickerPackId,
);
final stickerPack = GetIt.I
.get<stickers.StickersBloc>()
.state
.stickerPacks
.firstWhereOrNull(
(StickerPack pack) => pack.id == event.stickerPackId,
);
if (stickerPack == null) {
await _onRemoteStickerPackRequested(

View File

@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/painting.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/commands.dart';
@ -53,8 +53,7 @@ class StickersBloc extends Bloc<StickersEvent, StickersState> {
StickerPackRemovedEvent event,
Emitter<StickersState> emit,
) async {
final stickerPack = firstWhereOrNull(
state.stickerPacks,
final stickerPack = state.stickerPacks.firstWhereOrNull(
(StickerPack sp) => sp.id == event.stickerPackId,
)!;
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);

View File

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/awaitabledatasender.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/eventhandler.dart';

View File

@ -23,8 +23,8 @@ import 'package:moxxyv2/ui/pages/conversation/topbar.dart';
import 'package:moxxyv2/ui/pages/conversation/typing_indicator.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/theme.dart';
import 'package:moxxyv2/ui/widgets/chat/bubbles/bubbles.dart';
import 'package:moxxyv2/ui/widgets/chat/bubbles/date.dart';
import 'package:moxxyv2/ui/widgets/chat/bubbles/new_device.dart';
import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
import 'package:moxxyv2/ui/widgets/combined_picker.dart';
import 'package:moxxyv2/ui/widgets/context_menu.dart';
@ -232,10 +232,7 @@ class ConversationPageState extends State<ConversationPage>
constraints: BoxConstraints(
maxWidth: maxWidth,
),
child: NewDeviceBubble(
data: item.pseudoMessageData!,
title: state.conversation!.title,
),
child: bubbleFromPseudoMessageType(context, item),
),
],
);

View File

@ -107,30 +107,25 @@ class OwnDevicesPage extends StatelessWidget {
item.enabled,
item.verified,
hasVerifiedDevices,
onVerifiedPressed: !item.hasSessionWith
? null
: () async {
if (item.verified) return;
if (!item.hasSessionWith) return;
onVerifiedPressed: () async {
if (item.verified) return;
final uri = await scanXmppUriQrCode(context);
if (uri == null) return;
final uri = await scanXmppUriQrCode(context);
if (uri == null) return;
// ignore: use_build_context_synchronously
context.read<OwnDevicesBloc>().add(
DeviceVerifiedEvent(uri, item.deviceId),
);
},
onEnableValueChanged: !item.hasSessionWith
? null
: (value) {
context.read<OwnDevicesBloc>().add(
OwnDeviceEnabledSetEvent(
item.deviceId,
value,
),
);
},
// ignore: use_build_context_synchronously
context.read<OwnDevicesBloc>().add(
DeviceVerifiedEvent(uri, item.deviceId),
);
},
onEnableValueChanged: (value) {
context.read<OwnDevicesBloc>().add(
OwnDeviceEnabledSetEvent(
item.deviceId,
value,
),
);
},
onDeletePressed: () async {
final result = await showConfirmationDialog(
t.pages.profile.owndevices.deleteDeviceConfirmTitle,

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
import 'package:moxxyv2/ui/widgets/chat/bubbles/omemo.dart';
Widget bubbleFromPseudoMessageType(BuildContext context, Message message) {
assert(
message.pseudoMessageType != null,
'Message must have non-null pseudoMessageType',
);
switch (message.pseudoMessageType!) {
case PseudoMessageType.changedDevice:
final replacedAmount =
(message.pseudoMessageData?['ratchetsReplaced'] as int?) ?? 1;
return OmemoBubble(
text: t.pages.conversation.replacedDeviceMessage(n: replacedAmount),
onTap: () {
context.read<DevicesBloc>().add(
DevicesRequestedEvent(message.conversationJid),
);
},
);
case PseudoMessageType.newDevice:
final addedAmount =
(message.pseudoMessageData?['ratchetsAdded'] as int?) ?? 1;
return OmemoBubble(
text: t.pages.conversation.newDeviceMessage(n: addedAmount),
onTap: () {
context.read<DevicesBloc>().add(
DevicesRequestedEvent(message.conversationJid),
);
},
);
}
}

View File

@ -1,17 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
import 'package:moxxyv2/ui/constants.dart';
class NewDeviceBubble extends StatelessWidget {
const NewDeviceBubble({
required this.data,
required this.title,
class OmemoBubble extends StatelessWidget {
const OmemoBubble({
required this.text,
required this.onTap,
super.key,
});
final Map<String, dynamic> data;
final String title;
/// The text to display in the bubble.
final String text;
/// Callback for tapping the message.
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
@ -22,18 +23,14 @@ class NewDeviceBubble extends StatelessWidget {
child: Material(
color: bubbleColorNewDevice,
child: InkWell(
onTap: () {
context.read<DevicesBloc>().add(
DevicesRequestedEvent(data['jid']! as String),
);
},
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
child: Text(
t.pages.conversation.newDeviceMessage(title: title),
text,
style: const TextStyle(
color: Colors.black,
),

View File

@ -921,40 +921,40 @@ packages:
dependency: "direct main"
description:
name: moxlib
sha256: "135507494010803fba2d8a7333d323271c978559152882ca6ea695e5edbcd606"
sha256: "2a76a632d23ea73906964cee4463352995e40199036162217ea323a6c3846e73"
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
version: "0.1.5"
version: "0.2.0"
moxplatform:
dependency: "direct main"
description:
name: moxplatform
sha256: bd856b5b1cbdd45640aac22bc56747c5f1b0f3ca5b5e17767548e27a3f87eb2b
sha256: "6de28b4e358d09562f0c8275c565e0a25313003aa08501f1d6aa46350c1a1363"
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
version: "0.1.15"
version: "0.1.16"
moxplatform_android:
dependency: transitive
description:
name: moxplatform_android
sha256: "8b2ac2716afb970eb1a1af2a5fd5c6b8864ad08d44c85678d86fffdca27d373e"
sha256: f7af2e9f326dba6f712edc99aaa8b75f8b09332465f359f514333e79024c41de
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
version: "0.1.15"
version: "0.1.16"
moxplatform_platform_interface:
dependency: "direct main"
description:
name: moxplatform_platform_interface
sha256: "84bb5fb791567c1a197dba94dfad9e59e0839aa9a1e26359a70e0e9631ea71d6"
sha256: eba3f50a3fe00cd0d5fa9baae0f77a66017771c17ff1c376bd4c1ba04088bd5e
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
version: "0.1.15"
version: "0.1.16"
moxxmpp:
dependency: "direct main"
description:
path: "packages/moxxmpp"
ref: HEAD
resolved-ref: fa2ce7c2d10042246e19249f672ce32f90bab087
resolved-ref: "05e3d804a4036e9cd93fd27473a1e970fda3c3fc"
url: "https://codeberg.org/moxxy/moxxmpp.git"
source: git
version: "0.4.0"
@ -1009,11 +1009,12 @@ packages:
omemo_dart:
dependency: "direct main"
description:
name: omemo_dart
sha256: f255a0a16838b2a2373cf9739d2310909e79be179d894321b2c97d0531fc9614
url: "https://git.polynom.me/api/packages/PapaTutuWawa/pub/"
source: hosted
version: "0.4.3"
path: "."
ref: HEAD
resolved-ref: "49c7e114e6cf80dcde55fbbd218bba3182045862"
url: "https://github.com/PapaTutuWawa/omemo_dart.git"
source: git
version: "0.5.1"
package_config:
dependency: transitive
description:
@ -1198,6 +1199,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.4"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
protoc_plugin:
dependency: transitive
description:
name: protoc_plugin
sha256: e2be5014ba145dc0f8de20ac425afa2a513aff64fe350d338e481d40de0573df
url: "https://pub.dev"
source: hosted
version: "20.0.1"
provider:
dependency: transitive
description:

View File

@ -62,13 +62,13 @@ dependencies:
version: 0.1.4+1
moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.5
version: ^0.2.0
moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.15
version: 0.1.16
moxplatform_platform_interface:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.15
version: 0.1.16
moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.4.0
@ -81,7 +81,7 @@ dependencies:
native_imaging: 0.1.0
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: 0.4.3
version: 0.5.0
page_transition: 2.0.9
path: 1.8.2
path_provider: 2.0.11
@ -139,8 +139,13 @@ dependency_overrides:
moxxmpp:
git:
url: https://codeberg.org/moxxy/moxxmpp.git
rev: fa2ce7c2d10042246e19249f672ce32f90bab087
rev: 05e3d804a4036e9cd93fd27473a1e970fda3c3fc
path: packages/moxxmpp
omemo_dart:
git:
url: https://github.com/PapaTutuWawa/omemo_dart.git
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862
extra_licenses:
- name: undraw.co