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.", "stickerPickerNoStickersLine1": "You have no sticker packs installed.",
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.", "stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
"stickerSettings": "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...", "messageHint": "Send a message...",
"sendImages": "Send images", "sendImages": "Send images",
"sendFiles": "Send files", "sendFiles": "Send files",

View File

@ -170,7 +170,14 @@
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.", "stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.", "stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
"stickerSettings": "Stickereinstellungen", "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...", "messageHint": "Nachricht senden...",
"sendImages": "Bilder senden", "sendImages": "Bilder senden",
"sendFiles": "Dateien senden", "sendFiles": "Dateien senden",

View File

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

View File

@ -3,13 +3,6 @@ const messagesTable = 'Messages';
const rosterTable = 'RosterItems'; const rosterTable = 'RosterItems';
const mediaTable = 'SharedMedia'; const mediaTable = 'SharedMedia';
const preferenceTable = 'Preferences'; 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 xmppStateTable = 'XmppState';
const contactsTable = 'Contacts'; const contactsTable = 'Contacts';
const stickersTable = 'Stickers'; const stickersTable = 'Stickers';
@ -19,6 +12,10 @@ const subscriptionsTable = 'SubscriptionRequests';
const fileMetadataTable = 'FileMetadata'; const fileMetadataTable = 'FileMetadata';
const fileMetadataHashesTable = 'FileMetadataHashes'; const fileMetadataHashesTable = 'FileMetadataHashes';
const reactionsTable = 'Reactions'; const reactionsTable = 'Reactions';
const omemoDevicesTable = 'OmemoDevices';
const omemoDeviceListTable = 'OmemoDeviceList';
const omemoRatchetsTable = 'OmemoRatchets';
const omemoTrustTable = 'OmemoTrustTable';
const typeString = 0; const typeString = 0;
const typeInt = 1; const typeInt = 1;

View File

@ -18,7 +18,8 @@ Future<void> createDatabase(Database db, int version) async {
); );
// Messages // Messages
await db.execute(''' await db.execute(
'''
CREATE TABLE $messagesTable ( CREATE TABLE $messagesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL, sender TEXT NOT NULL,
@ -46,13 +47,15 @@ Future<void> createDatabase(Database db, int version) async {
pseudoMessageData TEXT, pseudoMessageData TEXT,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id) CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id) CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
)'''); )''',
);
await db.execute( await db.execute(
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)', 'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
); );
// Reactions // Reactions
await db.execute(''' await db.execute(
'''
CREATE TABLE $reactionsTable ( CREATE TABLE $reactionsTable (
senderJid TEXT NOT NULL, senderJid TEXT NOT NULL,
emoji 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 pk_sender PRIMARY KEY (senderJid, emoji, message_id),
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id) CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
ON DELETE CASCADE ON DELETE CASCADE
)'''); )''',
);
await db.execute( await db.execute(
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)', 'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
); );
// File metadata // File metadata
await db.execute(''' await db.execute(
'''
CREATE TABLE $fileMetadataTable ( CREATE TABLE $fileMetadataTable (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
path TEXT, path TEXT,
@ -83,8 +88,10 @@ Future<void> createDatabase(Database db, int version) async {
cipherTextHashes TEXT, cipherTextHashes TEXT,
filename TEXT NOT NULL, filename TEXT NOT NULL,
size INTEGER size INTEGER
)'''); )''',
await db.execute(''' );
await db.execute(
'''
CREATE TABLE $fileMetadataHashesTable ( CREATE TABLE $fileMetadataHashesTable (
algorithm TEXT NOT NULL, algorithm TEXT NOT NULL,
value 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 f_primarykey PRIMARY KEY (algorithm, value),
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id) CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
ON DELETE CASCADE ON DELETE CASCADE
)'''); )''',
);
await db.execute( await db.execute(
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)', '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 ( CREATE TABLE $conversationsTable (
jid TEXT NOT NULL PRIMARY KEY, jid TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
avatarUrl TEXT NOT NULL, avatarPath TEXT NOT NULL,
avatarHash TEXT,
type TEXT NOT NULL, type TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL, lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL, unreadCounter INTEGER NOT NULL,
@ -124,11 +133,13 @@ Future<void> createDatabase(Database db, int version) async {
); );
// Contacts // Contacts
await db.execute(''' await db.execute(
'''
CREATE TABLE $contactsTable ( CREATE TABLE $contactsTable (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
jid TEXT NOT NULL jid TEXT NOT NULL
)'''); )''',
);
// Roster // Roster
await db.execute( await db.execute(
@ -137,7 +148,7 @@ Future<void> createDatabase(Database db, int version) async {
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL, jid TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
avatarUrl TEXT NOT NULL, avatarPath TEXT NOT NULL,
avatarHash TEXT NOT NULL, avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL, subscription TEXT NOT NULL,
ask TEXT NOT NULL, ask TEXT NOT NULL,
@ -188,72 +199,58 @@ Future<void> createDatabase(Database db, int version) async {
// OMEMO // OMEMO
await db.execute( await db.execute(
''' '''
CREATE TABLE $omemoRatchetsTable ( CREATE TABLE $omemoDevicesTable (
id INTEGER NOT NULL, jid TEXT NOT NULL PRIMARY KEY,
jid TEXT NOT NULL, id INTEGER NOT NULL,
dhs TEXT NOT NULL, ikPub TEXT NOT NULL,
dhs_pub TEXT NOT NULL, ik TEXT NOT NULL,
dhr TEXT, spkPub TEXT NOT NULL,
rk TEXT NOT NULL, spk TEXT NOT NULL,
cks TEXT, spkId INTEGER NOT NULL,
ckr TEXT, spkSig TEXT NOT NULL,
ns INTEGER NOT NULL, oldSpkPub TEXT,
nr INTEGER NOT NULL, oldSpk TEXT,
pn INTEGER NOT NULL, oldSpkId INTEGER,
ik_pub TEXT NOT NULL, opks 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)
)''', )''',
); );
await db.execute( await db.execute(
''' '''
CREATE TABLE $omemoDeviceListTable ( CREATE TABLE $omemoDeviceListTable (
jid TEXT NOT NULL, jid TEXT NOT NULL PRIMARY KEY,
id INTEGER NOT NULL, devices TEXT NOT NULL
PRIMARY KEY (jid, id)
)''', )''',
); );
await db.execute( await db.execute(
''' '''
CREATE TABLE $omemoFingerprintCache ( CREATE TABLE $omemoRatchetsTable (
jid TEXT NOT NULL, jid TEXT NOT NULL,
id INTEGER NOT NULL, device INTEGER NOT NULL,
fingerprint TEXT NOT NULL, dhsPub TEXT NOT NULL,
PRIMARY KEY (jid, id) 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_shared_media.dart';
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.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_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:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
@ -148,6 +150,8 @@ const List<DatabaseMigration<Database>> migrations = [
DatabaseMigration(37, upgradeFromV36ToV37), DatabaseMigration(37, upgradeFromV36ToV37),
DatabaseMigration(38, upgradeFromV37ToV38), DatabaseMigration(38, upgradeFromV37ToV38),
DatabaseMigration(39, upgradeFromV38ToV39), DatabaseMigration(39, upgradeFromV38ToV39),
DatabaseMigration(40, upgradeFromV39ToV40),
DatabaseMigration(41, upgradeFromV40ToV41),
]; ];
class DatabaseService { class DatabaseService {

View File

@ -1,10 +1,9 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV12ToV13(Database db) async { Future<void> upgradeFromV12ToV13(Database db) async {
await db.execute( await db.execute(
''' '''
CREATE TABLE $omemoFingerprintCache ( CREATE TABLE OmemoFingerprintCache (
jid TEXT NOT NULL, jid TEXT NOT NULL,
id INTEGER NOT NULL, id INTEGER NOT NULL,
fingerprint TEXT 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>(); final omemo = GetIt.I.get<OmemoService>();
sendEvent( sendEvent(
GetConversationOmemoFingerprintsResult( GetConversationOmemoFingerprintsResult(
fingerprints: await omemo.getOmemoKeysForJid(command.jid), fingerprints: await omemo.getFingerprintsForJid(command.jid),
), ),
id: id, id: id,
); );
@ -789,7 +789,7 @@ Future<void> performEnableOmemoKey(
final id = extra as String; final id = extra as String;
final omemo = GetIt.I.get<OmemoService>(); final omemo = GetIt.I.get<OmemoService>();
await omemo.setOmemoKeyEnabled( await omemo.setDeviceEnablement(
command.jid, command.jid,
command.deviceId, command.deviceId,
command.enabled, command.enabled,
@ -805,10 +805,14 @@ Future<void> performRecreateSessions(
RecreateSessionsCommand command, { RecreateSessionsCommand command, {
dynamic extra, dynamic extra,
}) async { }) 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>(); // And force the creation of new ones
await conn.getManagerById<BaseOmemoManager>(omemoManager)!.sendOmemoHeartbeat( await GetIt.I
.get<XmppConnection>()
.getManagerById<OmemoManager>(omemoManager)!
.sendOmemoHeartbeat(
command.jid, command.jid,
); );
} }
@ -837,14 +841,14 @@ Future<void> performGetOwnOmemoFingerprints(
final id = extra as String; final id = extra as String;
final os = GetIt.I.get<OmemoService>(); final os = GetIt.I.get<OmemoService>();
final xs = GetIt.I.get<XmppService>(); final xs = GetIt.I.get<XmppService>();
await os.ensureInitialized();
final jid = (await xs.getConnectionSettings())!.jid; final jid = (await xs.getConnectionSettings())!.jid;
final device = await os.getDevice();
sendEvent( sendEvent(
GetOwnOmemoFingerprintsResult( GetOwnOmemoFingerprintsResult(
ownDeviceFingerprint: await os.getDeviceFingerprint(), ownDeviceFingerprint: await device.getFingerprint(),
ownDeviceId: await os.getDeviceId(), ownDeviceId: device.id,
fingerprints: await os.getOwnFingerprints(jid), fingerprints: await os.getFingerprintsForJid(jid.toString()),
), ),
id: id, id: id,
); );
@ -856,7 +860,7 @@ Future<void> performRemoveOwnDevice(
}) async { }) async {
await GetIt.I await GetIt.I
.get<XmppConnection>() .get<XmppConnection>()
.getManagerById<BaseOmemoManager>(omemoManager)! .getManagerById<OmemoManager>(omemoManager)!
.deleteDevice(command.deviceId); .deleteDevice(command.deviceId);
} }
@ -865,9 +869,7 @@ Future<void> performRegenerateOwnDevice(
dynamic extra, dynamic extra,
}) async { }) async {
final id = extra as String; final id = extra as String;
final jid = final device = await GetIt.I.get<OmemoService>().regenerateDevice();
GetIt.I.get<XmppConnection>().connectionSettings.jid.toBare().toString();
final device = await GetIt.I.get<OmemoService>().regenerateDevice(jid);
sendEvent( sendEvent(
RegenerateOwnDeviceResult(device: device), RegenerateOwnDeviceResult(device: device),
@ -1041,9 +1043,9 @@ Future<void> performMarkDeviceVerified(
MarkOmemoDeviceAsVerifiedCommand command, { MarkOmemoDeviceAsVerifiedCommand command, {
dynamic extra, dynamic extra,
}) async { }) async {
await GetIt.I.get<OmemoService>().verifyDevice( await GetIt.I.get<OmemoService>().setDeviceVerified(
command.deviceId,
command.jid, 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 isDownloading = false,
bool isUploading = false, bool isUploading = false,
String? stickerPackId, String? stickerPackId,
int? pseudoMessageType, PseudoMessageType? pseudoMessageType,
Map<String, dynamic>? pseudoMessageData, Map<String, dynamic>? pseudoMessageData,
bool received = false, bool received = false,
bool displayed = 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:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:hex/hex.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart' as moxxmpp; 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/message.dart';
import 'package:moxxyv2/service/moxxmpp/omemo.dart';
import 'package:moxxyv2/service/omemo/implementations.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/service.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/models/omemo_device.dart' as model; import 'package:moxxyv2/shared/models/omemo_device.dart' as model;
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:synchronized/synchronized.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 { class OmemoService {
/// Logger.
final Logger _log = Logger('OmemoService'); final Logger _log = Logger('OmemoService');
/// Flag indicating whether we are initialized.
bool _initialized = false; 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(); final Lock _lock = Lock();
/// Queue for code that is waiting on the service initialization.
final Queue<Completer<void>> _waitingForInitialization = final Queue<Completer<void>> _waitingForInitialization =
Queue<Completer<void>>(); 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 { /// Access the underlying [OmemoManager].
final done = await _lock.synchronized(() => _initialized); Future<OmemoManager> getOmemoManager() async {
if (done) return; await ensureInitialized();
return _omemoManager;
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(),
);
} }
/// Ensures that the code following this *AWAITED* call can access every method /// 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 { /// Creates or loads the [OmemoManager] for the JID [jid].
await _saveOmemoDeviceList(deviceMap); 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 { Future<moxxmpp.OmemoError?> publishDeviceIfNeeded() 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 {
_log.finest('publishDeviceIfNeeded: Waiting for initialization...'); _log.finest('publishDeviceIfNeeded: Waiting for initialization...');
await ensureInitialized(); await ensureInitialized();
_log.finest('publishDeviceIfNeeded: Done'); _log.finest('publishDeviceIfNeeded: Done');
final conn = GetIt.I.get<moxxmpp.XmppConnection>(); final conn = GetIt.I.get<moxxmpp.XmppConnection>();
final omemo = final omemo =
conn.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!; conn.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!;
final dm = conn.getManagerById<moxxmpp.DiscoManager>(moxxmpp.discoManager)!; final dm = conn.getManagerById<moxxmpp.DiscoManager>(moxxmpp.discoManager)!;
final bareJid = conn.connectionSettings.jid.toBare(); final bareJid = conn.connectionSettings.jid.toBare();
final device = await omemoManager.getDevice(); final device = await _omemoManager.getDevice();
final bundlesRaw = await dm.discoItemsQuery( final bundlesRaw = await dm.discoItemsQuery(
bareJid, bareJid,
@ -256,7 +138,7 @@ class OmemoService {
); );
if (bundlesRaw.isType<moxxmpp.DiscoError>()) { if (bundlesRaw.isType<moxxmpp.DiscoError>()) {
await omemo.publishBundle(await device.toBundle()); await omemo.publishBundle(await device.toBundle());
return bundlesRaw.get<moxxmpp.DiscoError>(); return null;
} }
final bundleIds = bundlesRaw final bundleIds = bundlesRaw
@ -285,469 +167,114 @@ class OmemoService {
return null; return null;
} }
Future<void> _fetchFingerprintsAndCache(moxxmpp.JID jid) async { Future<void> onNewConnection() 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 {
await ensureInitialized(); await ensureInitialized();
await _omemoManager.onNewConnection();
// 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;
} }
Future<void> commitTrustManager(Map<String, dynamic> json) async { Future<List<model.OmemoDevice>> getFingerprintsForJid(String jid) 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 {
await ensureInitialized(); 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 _omemoManager.withTrustManager(
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(
jid, jid,
deviceId, (tm) async {
BTBVTrustState.verified, trust = await (tm as BlindTrustBeforeVerificationTrustManager)
.getDevicesTrust(jid);
},
); );
}
/// Tells omemo_dart, that certain caches are to be seen as invalidated. return fingerprints.map((fp) {
void onNewConnection() { return model.OmemoDevice(
if (_initialized) { fp.fingerprint,
omemoManager.onNewConnection(); trust[fp.deviceId]?.trusted ?? false,
} trust[fp.deviceId]?.state == BTBVTrustState.verified,
} trust[fp.deviceId]?.enabled ?? false,
fp.deviceId,
/// 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,
); );
}).toList(); }).toList();
} }
Future<void> _saveRatchet(OmemoDoubleRatchetWrapper ratchet) async { Future<void> setDeviceEnablement(String jid, int device, bool state) async {
final json = await ratchet.ratchet.toJson(); await ensureInitialized();
await GetIt.I.get<DatabaseService>().database.insert( await _omemoManager.withTrustManager(jid, (tm) async {
omemoRatchetsTable, await (tm as BlindTrustBeforeVerificationTrustManager)
{ .setEnabled(jid, device, state);
...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,
);
}); });
return Map.fromEntries(mapEntries);
} }
Future<void> _saveTrustCache(Map<String, int> cache) async { Future<void> setDeviceVerified(String jid, int device) async {
final batch = GetIt.I.get<DatabaseService>().database.batch(); await ensureInitialized();
await _omemoManager.withTrustManager(jid, (tm) async {
// ignore: cascade_invocations await (tm as BlindTrustBeforeVerificationTrustManager)
batch.delete(omemoTrustCacheTable); .setDeviceTrust(jid, device, BTBVTrustState.verified);
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),
);
}); });
return Map.fromEntries(mapEntries);
} }
Future<void> _saveTrustEnablementList(Map<String, bool> list) async { Future<void> removeAllRatchets(String jid) async {
final batch = GetIt.I.get<DatabaseService>().database.batch(); await ensureInitialized();
await _omemoManager.removeAllRatchets(jid);
// 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<Map<String, List<int>>> _loadTrustDeviceList() async { Future<OmemoDevice> getDevice() async {
final entries = await GetIt.I await ensureInitialized();
.get<DatabaseService>() return _omemoManager.getDevice();
.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<void> _saveTrustDeviceList(Map<String, List<int>> list) async { Future<model.OmemoDevice> regenerateDevice() async {
final batch = GetIt.I.get<DatabaseService>().database.batch(); await ensureInitialized();
// ignore: cascade_invocations final oldDeviceId = (await getDevice()).id;
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,
);
}
}
await batch.commit(); // Generate the new device
} final newDevice = await _omemoManager.regenerateDevice();
Future<void> _saveOmemoDevice(OmemoDevice device) async { // Remove the old device
await GetIt.I.get<DatabaseService>().database.insert( unawaited(
omemoDeviceTable, GetIt.I
{ .get<moxxmpp.XmppConnection>()
'jid': device.jid, .getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!
'id': device.id, .deleteDevice(oldDeviceId),
'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],
); );
return rawItems.map((item) { return model.OmemoDevice(
return OmemoCacheTriple( await newDevice.getFingerprint(),
jid, true,
item['id']! as int, true,
item['fingerprint']! as String, true,
); newDevice.id,
}).toList(); );
}
/// 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/language.dart';
import 'package:moxxyv2/service/message.dart'; import 'package:moxxyv2/service/message.dart';
import 'package:moxxyv2/service/moxxmpp/connectivity.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/roster.dart';
import 'package:moxxyv2/service/moxxmpp/socket.dart'; import 'package:moxxyv2/service/moxxmpp/socket.dart';
import 'package:moxxyv2/service/moxxmpp/stream.dart'; import 'package:moxxyv2/service/moxxmpp/stream.dart';
@ -222,7 +221,12 @@ Future<void> entrypoint() async {
const Identity(category: 'client', type: 'phone', name: 'Moxxy'), const Identity(category: 'client', type: 'phone', name: 'Moxxy'),
]), ]),
RosterManager(MoxxyRosterStateManager()), RosterManager(MoxxyRosterStateManager()),
MoxxyOmemoManager(), OmemoManager(
GetIt.I.get<OmemoService>().getOmemoManager,
(toJid, _) async => GetIt.I
.get<ConversationService>()
.shouldEncryptForConversation(toJid),
),
PingManager(const Duration(minutes: 3)), PingManager(const Duration(minutes: 3)),
MessageManager(), MessageManager(),
PresenceManager(), PresenceManager(),

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -313,8 +314,7 @@ class XmppService {
// }, // },
// ); // );
final hasUrlSource = firstWhereOrNull( final hasUrlSource = sfs!.sources.firstWhereOrNull(
sfs!.sources,
(src) => src is StatelessFileSharingUrlSource, (src) => src is StatelessFileSharingUrlSource,
) != ) !=
null; null;
@ -336,8 +336,7 @@ class XmppService {
sfs.metadata.size, sfs.metadata.size,
); );
} else { } else {
final encryptedSource = firstWhereOrNull( final encryptedSource = sfs.sources.firstWhereOrNull(
sfs.sources,
(src) => src is StatelessFileSharingEncryptedSource, (src) => src is StatelessFileSharingEncryptedSource,
)! as StatelessFileSharingEncryptedSource; )! as StatelessFileSharingEncryptedSource;
@ -387,22 +386,24 @@ class XmppService {
final manager = GetIt.I final manager = GetIt.I
.get<XmppConnection>() .get<XmppConnection>()
.getManagerById<MessageManager>(messageManager)!; .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( await manager.sendMessage(
event.from.toBare(), event.from.toBare(),
TypedMap<StanzaHandlerExtension>.fromList([ TypedMap<StanzaHandlerExtension>.fromList([
ChatMarkerData( ChatMarkerData(
ChatMarker.received, ChatMarker.received,
originId ?? event.id, originId ?? event.id!,
) )
]), ]),
); );
} else if (deliveryReceiptRequested && } else if (deliveryReceiptRequested &&
info.features.contains(deliveryXmlns)) { info.features.contains(deliveryXmlns) &&
hasId) {
await manager.sendMessage( await manager.sendMessage(
event.from.toBare(), event.from.toBare(),
TypedMap<StanzaHandlerExtension>.fromList([ TypedMap<StanzaHandlerExtension>.fromList([
MessageDeliveryReceivedData(originId ?? event.id), MessageDeliveryReceivedData(originId ?? event.id!),
]), ]),
); );
} }
@ -780,7 +781,9 @@ class XmppService {
GetIt.I.get<BlocklistService>().onNewConnection(); GetIt.I.get<BlocklistService>().onNewConnection();
// Reset the OMEMO cache // 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) // Enable carbons, if they're not already enabled (e.g. by using SASL2)
final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!; final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!;
@ -1054,10 +1057,17 @@ class XmppService {
return; return;
} }
if (event.id == null) {
_log.warning(
'Received error message without id.',
);
return;
}
final ms = GetIt.I.get<MessageService>(); final ms = GetIt.I.get<MessageService>();
final msg = await ms.getMessageByStanzaId( final msg = await ms.getMessageByStanzaId(
event.from.toBare().toString(), event.from.toBare().toString(),
event.id, event.id!,
); );
if (msg == null) { if (msg == null) {
@ -1214,7 +1224,7 @@ class XmppService {
// Stop the processing here if the event does not describe a displayable message // Stop the processing here if the event does not describe a displayable message
if (!_isMessageEventMessage(event) && event.encryptionError == null) return; 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. // Ignore File Upload Notifications where we don't have a filename.
final fun = event.extensions.get<FileUploadNotificationData>(); final fun = event.extensions.get<FileUploadNotificationData>();
@ -1234,8 +1244,6 @@ class XmppService {
final isInRoster = rosterItem != null; final isInRoster = rosterItem != null;
// True if the message was sent by us (via a Carbon) // True if the message was sent by us (via a Carbon)
final sent = isCarbon && event.from.toBare().toString() == state.jid; 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 // Acknowledge the message if enabled
final receiptRequested = 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 // 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>(); final ms = GetIt.I.get<MessageService>();
var message = await ms.addMessageFromData( var message = await ms.addMessageFromData(
messageBody, messageBody,
messageTimestamp, messageTimestamp,
event.from.toString(), event.from.toString(),
conversationJid, conversationJid,
event.id, // TODO(Unknown): Should we handle this differently?
event.id ?? '',
fun != null, fun != null,
event.encrypted, event.encrypted,
event.extensions 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:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/models/preferences.dart'; import 'package:moxxyv2/shared/models/preferences.dart';

View File

@ -25,11 +25,9 @@ int errorTypeFromException(dynamic exception) {
return noError; return noError;
} }
if (exception is NoDecryptionKeyException) { if (exception is InvalidMessageHMACError) {
return messageNoDecryptionKey;
} else if (exception is InvalidMessageHMACException) {
return messageInvalidHMAC; return messageInvalidHMAC;
} else if (exception is NotEncryptedForDeviceException) { } else if (exception is NotEncryptedForDeviceError) {
return messageNoDecryptionKey; return messageNoDecryptionKey;
} else if (exception is InvalidAffixElementsException) { } else if (exception is InvalidAffixElementsException) {
return messageInvalidAffixElements; 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:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/models/conversation.dart'; import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/message.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.freezed.dart';
part 'message.g.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) { Map<String, dynamic> _optionalJsonDecodeWithFallback(String? data) {
if (data == null) return <String, dynamic>{}; if (data == null) return <String, dynamic>{};
@ -53,7 +76,7 @@ class Message with _$Message {
Message? quotes, Message? quotes,
@Default([]) List<String> reactionsPreview, @Default([]) List<String> reactionsPreview,
String? stickerPackId, String? stickerPackId,
int? pseudoMessageType, PseudoMessageType? pseudoMessageType,
Map<String, dynamic>? pseudoMessageData, Map<String, dynamic>? pseudoMessageData,
}) = _Message; }) = _Message;
@ -83,6 +106,13 @@ class Message with _$Message {
'isEdited': intToBool(json['isEdited']! as int), 'isEdited': intToBool(json['isEdited']! as int),
'containsNoStore': intToBool(json['containsNoStore']! as int), 'containsNoStore': intToBool(json['containsNoStore']! as int),
'reactionsPreview': reactionsPreview, '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': 'pseudoMessageData':
_optionalJsonDecodeWithFallback(json['pseudoMessageData'] as String?) _optionalJsonDecodeWithFallback(json['pseudoMessageData'] as String?)
}).copyWith( }).copyWith(
@ -114,6 +144,7 @@ class Message with _$Message {
'isRetracted': boolToInt(isRetracted), 'isRetracted': boolToInt(isRetracted),
'isEdited': boolToInt(isEdited), 'isEdited': boolToInt(isEdited),
'containsNoStore': boolToInt(containsNoStore), 'containsNoStore': boolToInt(containsNoStore),
'pseudoMessageType': pseudoMessageType?.id,
'pseudoMessageData': _optionalJsonEncodeWithFallback(pseudoMessageData), 'pseudoMessageData': _optionalJsonEncodeWithFallback(pseudoMessageData),
}; };
} }

View File

@ -11,9 +11,8 @@ class OmemoDevice with _$OmemoDevice {
bool trusted, bool trusted,
bool verified, bool verified,
bool enabled, bool enabled,
int deviceId, { int deviceId,
@Default(true) bool hasSessionWith, ) = _OmemoDevice;
}) = _OmemoDevice;
/// JSON /// JSON
factory OmemoDevice.fromJson(Map<String, dynamic> 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.freezed.dart';
part 'xmpp_state.g.dart'; part 'xmpp_state.g.dart';
extension StreamManagementStateToJson on StreamManagementState {
Map<String, dynamic> toJson() => {
'c2s': c2s,
's2c': s2c,
'streamResumptionLocation': streamResumptionLocation,
'streamResumptionId': streamResumptionId,
};
}
class StreamManagementStateConverter class StreamManagementStateConverter
implements JsonConverter<StreamManagementState, Map<String, dynamic>> { implements JsonConverter<StreamManagementState, Map<String, dynamic>> {
const StreamManagementStateConverter(); const StreamManagementStateConverter();
@override @override
StreamManagementState fromJson(Map<String, dynamic> json) => 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 @override
Map<String, dynamic> toJson(StreamManagementState state) => state.toJson(); Map<String, dynamic> toJson(StreamManagementState state) => state.toJson();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/eventhandler.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/pages/conversation/typing_indicator.dart';
import 'package:moxxyv2/ui/service/data.dart'; import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/theme.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/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/chat/chatbubble.dart';
import 'package:moxxyv2/ui/widgets/combined_picker.dart'; import 'package:moxxyv2/ui/widgets/combined_picker.dart';
import 'package:moxxyv2/ui/widgets/context_menu.dart'; import 'package:moxxyv2/ui/widgets/context_menu.dart';
@ -232,10 +232,7 @@ class ConversationPageState extends State<ConversationPage>
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: maxWidth, maxWidth: maxWidth,
), ),
child: NewDeviceBubble( child: bubbleFromPseudoMessageType(context, item),
data: item.pseudoMessageData!,
title: state.conversation!.title,
),
), ),
], ],
); );

View File

@ -107,30 +107,25 @@ class OwnDevicesPage extends StatelessWidget {
item.enabled, item.enabled,
item.verified, item.verified,
hasVerifiedDevices, hasVerifiedDevices,
onVerifiedPressed: !item.hasSessionWith onVerifiedPressed: () async {
? null if (item.verified) return;
: () async {
if (item.verified) return;
if (!item.hasSessionWith) return;
final uri = await scanXmppUriQrCode(context); final uri = await scanXmppUriQrCode(context);
if (uri == null) return; if (uri == null) return;
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context.read<OwnDevicesBloc>().add( context.read<OwnDevicesBloc>().add(
DeviceVerifiedEvent(uri, item.deviceId), DeviceVerifiedEvent(uri, item.deviceId),
); );
}, },
onEnableValueChanged: !item.hasSessionWith onEnableValueChanged: (value) {
? null context.read<OwnDevicesBloc>().add(
: (value) { OwnDeviceEnabledSetEvent(
context.read<OwnDevicesBloc>().add( item.deviceId,
OwnDeviceEnabledSetEvent( value,
item.deviceId, ),
value, );
), },
);
},
onDeletePressed: () async { onDeletePressed: () async {
final result = await showConfirmationDialog( final result = await showConfirmationDialog(
t.pages.profile.owndevices.deleteDeviceConfirmTitle, 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/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'; import 'package:moxxyv2/ui/constants.dart';
class NewDeviceBubble extends StatelessWidget { class OmemoBubble extends StatelessWidget {
const NewDeviceBubble({ const OmemoBubble({
required this.data, required this.text,
required this.title, required this.onTap,
super.key, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,18 +23,14 @@ class NewDeviceBubble extends StatelessWidget {
child: Material( child: Material(
color: bubbleColorNewDevice, color: bubbleColorNewDevice,
child: InkWell( child: InkWell(
onTap: () { onTap: onTap,
context.read<DevicesBloc>().add(
DevicesRequestedEvent(data['jid']! as String),
);
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 6, vertical: 6,
), ),
child: Text( child: Text(
t.pages.conversation.newDeviceMessage(title: title), text,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
), ),

View File

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

View File

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