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:
commit
8913977c0a
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
)''',
|
||||
);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
72
lib/service/database/migrations/0003_new_omemo.dart
Normal file
72
lib/service/database/migrations/0003_new_omemo.dart
Normal 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)
|
||||
)''',
|
||||
);
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
308
lib/service/omemo/persistence.dart
Normal file
308
lib/service/omemo/persistence.dart
Normal 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],
|
||||
);
|
||||
}
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
|
38
lib/ui/widgets/chat/bubbles/bubbles.dart
Normal file
38
lib/ui/widgets/chat/bubbles/bubbles.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
45
pubspec.lock
45
pubspec.lock
@ -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:
|
||||
|
15
pubspec.yaml
15
pubspec.yaml
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user