From dba577f39c489b9db29960f2bf5cc78ac4f29d7b Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 4 Oct 2026 14:42:16 +0200 Subject: Move OMEMO, OpenPGP and RTP plugins into main/libdino --- main/src/plugins/omemo/logic/database.vala | 284 +++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 main/src/plugins/omemo/logic/database.vala (limited to 'main/src/plugins/omemo/logic/database.vala') diff --git a/main/src/plugins/omemo/logic/database.vala b/main/src/plugins/omemo/logic/database.vala new file mode 100644 index 00000000..f2a92259 --- /dev/null +++ b/main/src/plugins/omemo/logic/database.vala @@ -0,0 +1,284 @@ +using Gee; + +using Dino.Entities; +using Dino.Qlite; + +namespace Dino.Plugins.Omemo { + +public class Database : Qlite.Database { + private const int VERSION = 5; + + public class IdentityMetaTable : Table { + //Default to provide backwards compatability + public Column identity_id = new Column.Integer("identity_id") { not_null = true, min_version = 2, default = "-1" }; + public Column address_name = new Column.Text("address_name") { not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column identity_key_public_base64 = new Column.Text("identity_key_public_base64"); + public Column trusted_identity = new Column.BoolInt("trusted_identity") { default = "0", max_version = 1 }; + public Column trust_level = new Column.Integer("trust_level") { default = TrustLevel.UNKNOWN.to_string(), min_version = 2 }; + public Column now_active = new Column.BoolInt("now_active") { default = "1" }; + public Column last_active = new Column.Long("last_active"); + public Column last_message_untrusted = new Column.Long("last_message_untrusted") { min_version = 5 }; + public Column last_message_undecryptable = new Column.Long("last_message_undecryptable") { min_version = 5 }; + + internal IdentityMetaTable(Database db) { + base(db, "identity_meta"); + init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active, last_message_untrusted, last_message_undecryptable}); + index("identity_meta_idx", {identity_id, address_name, device_id}, true); + index("identity_meta_list_idx", {identity_id, address_name}); + } + + public QueryBuilder with_address(int identity_id, string address_name) { + return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name); + } + + public QueryBuilder get_with_device_id(int identity_id, int device_id) { + return select().with(this.identity_id, "=", identity_id).with(this.device_id, "=", device_id); + } + + public void insert_device_list(int32 identity_id, string address_name, ArrayList devices) { + update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform(); + foreach (int32 device_id in devices) { + upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.now_active, true) + .value(this.last_active, (long) new DateTime.now_utc().to_unix()) + .perform(); + } + } + + public int64 insert_device_bundle(int32 identity_id, string address_name, int device_id, Bundle bundle, TrustLevel trust) { + if (bundle == null || bundle.identity_key == null) return -1; + // Do not replace identity_key if it was known before, it should never change! + string identity_key = Base64.encode(bundle.identity_key.serialize()); + RowOption row = with_address(identity_id, address_name).with(this.device_id, "=", device_id).single().row(); + if (row.is_present() && row[identity_key_public_base64] != null && row[identity_key_public_base64] != identity_key) { + critical("Tried to change the identity key for a known device id. Likely an attack."); + return -1; + } + return upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.identity_key_public_base64, identity_key) + .value(this.trust_level, trust).perform(); + } + + public int64 insert_device_session(int32 identity_id, string address_name, int device_id, string identity_key, TrustLevel trust) { + RowOption row = with_address(identity_id, address_name).with(this.device_id, "=", device_id).single().row(); + if (row.is_present() && row[identity_key_public_base64] != null && row[identity_key_public_base64] != identity_key) { + critical("Tried to change the identity key for a known device id. Likely an attack."); + return -1; + } + return upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.identity_key_public_base64, identity_key) + .value(this.trust_level, trust).perform(); + } + + public void update_last_message_untrusted(int identity_id, int device_id, DateTime? time) { + var stmt = update() + .with(this.identity_id, "=", identity_id) + .with(this.device_id, "=", device_id); + if (time != null) { + stmt.set(last_message_untrusted, (long)time.to_unix()); + } else { + stmt.set_null(last_message_untrusted); + } + stmt.perform(); + } + + public void update_last_message_undecryptable(int identity_id, int device_id, DateTime? time) { + var stmt = update() + .with(this.identity_id, "=", identity_id) + .with(this.device_id, "=", device_id); + if (time != null) { + stmt.set(last_message_undecryptable, (long)time.to_unix()); + } else { + stmt.set_null(last_message_undecryptable); + } + stmt.perform(); + } + + public QueryBuilder get_trusted_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "!=", TrustLevel.UNTRUSTED) + .with(this.now_active, "=", true); + } + + public QueryBuilder get_known_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "!=", TrustLevel.UNKNOWN) + .without_null(this.identity_key_public_base64); + } + + public QueryBuilder get_unknown_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with_null(this.identity_key_public_base64); + } + + public QueryBuilder get_new_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "=", TrustLevel.UNKNOWN) + .without_null(this.identity_key_public_base64); + } + + public Row? get_device(int identity_id, string address_name, int device_id) { + return this.with_address(identity_id, address_name) + .with(this.device_id, "=", device_id).single().row().inner; + } + } + + + public class TrustTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.Text("address_name"); + public Column blind_trust = new Column.BoolInt("blind_trust") { default = "1" } ; + + internal TrustTable(Database db) { + base(db, "trust"); + init({identity_id, address_name, blind_trust}); + index("trust_idx", {identity_id, address_name}, true); + } + + public bool get_blind_trust(int32 identity_id, string address_name, bool def = false) { + RowOption row = this.select().with(this.identity_id, "=", identity_id) + .with(this.address_name, "=", address_name).single().row(); + if (row.is_present()) return row[blind_trust]; + return def; + } + } + + public class IdentityTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column account_id = new Column.Integer("account_id") { unique = true, not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column identity_key_private_base64 = new Column.NonNullText("identity_key_private_base64"); + public Column identity_key_public_base64 = new Column.NonNullText("identity_key_public_base64"); + + internal IdentityTable(Database db) { + base(db, "identity"); + init({id, account_id, device_id, identity_key_private_base64, identity_key_public_base64}); + } + + public int get_id(int account_id) { + int id = -1; + Row? row = this.row_with(this.account_id, account_id).inner; + if (row != null) id = ((!)row)[this.id]; + return id; + } + } + + public class SignedPreKeyTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column signed_pre_key_id = new Column.Integer("signed_pre_key_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal SignedPreKeyTable(Database db) { + base(db, "signed_pre_key"); + init({identity_id, signed_pre_key_id, record_base64}); + unique({identity_id, signed_pre_key_id}); + index("signed_pre_key_idx", {identity_id, signed_pre_key_id}, true); + } + } + + public class PreKeyTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column pre_key_id = new Column.Integer("pre_key_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal PreKeyTable(Database db) { + base(db, "pre_key"); + init({identity_id, pre_key_id, record_base64}); + unique({identity_id, pre_key_id}); + index("pre_key_idx", {identity_id, pre_key_id}, true); + } + } + + public class SessionTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.NonNullText("name"); + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal SessionTable(Database db) { + base(db, "session"); + init({identity_id, address_name, device_id, record_base64}); + unique({identity_id, address_name, device_id}); + index("session_idx", {identity_id, address_name, device_id}, true); + } + } + + public class ContentItemMetaTable : Table { + public Column content_item_id = new Column.Integer("message_id") { primary_key = true }; + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.Text("address_name") { not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column trusted_when_received = new Column.BoolInt("trusted_when_received") { not_null = true, default = "1" }; + + internal ContentItemMetaTable(Database db) { + base(db, "content_item_meta"); + init({content_item_id, identity_id, address_name, device_id, trusted_when_received}); + index("content_item_meta_device_idx", {identity_id, device_id, address_name}); + } + + public RowOption with_content_item(ContentItem item) { + return row_with(content_item_id, item.id); + } + + public QueryBuilder with_device(int identity_id, string address_name, int device_id) { + return select() + .with(this.identity_id, "=", identity_id) + .with(this.address_name, "=", address_name) + .with(this.device_id, "=", device_id); + } + } + + public IdentityMetaTable identity_meta { get; private set; } + public TrustTable trust { get; private set; } + public IdentityTable identity { get; private set; } + public SignedPreKeyTable signed_pre_key { get; private set; } + public PreKeyTable pre_key { get; private set; } + public SessionTable session { get; private set; } + public ContentItemMetaTable content_item_meta { get; private set; } + + public Database(string fileName) { + base(fileName, VERSION); + identity_meta = new IdentityMetaTable(this); + trust = new TrustTable(this); + identity = new IdentityTable(this); + signed_pre_key = new SignedPreKeyTable(this); + pre_key = new PreKeyTable(this); + session = new SessionTable(this); + content_item_meta = new ContentItemMetaTable(this); + init({identity_meta, trust, identity, signed_pre_key, pre_key, session, content_item_meta}); + + try { + exec("PRAGMA journal_mode = WAL"); + exec("PRAGMA synchronous = NORMAL"); + exec("PRAGMA secure_delete = ON"); + } catch (Error e) { + error("Failed to set OMEMO database properties: %s", e.message); + } + } + + public override void migrate(long oldVersion) { + if(oldVersion == 1) { + try { + exec("DROP INDEX identity_meta_idx"); + exec("DROP INDEX identity_meta_list_idx"); + exec("CREATE UNIQUE INDEX identity_meta_idx ON identity_meta (identity_id, address_name, device_id)"); + exec("CREATE INDEX identity_meta_list_idx ON identity_meta (identity_id, address_name)"); + } catch (Error e) { + stderr.printf("Failed to migrate OMEMO database\n"); + Process.exit(-1); + } + } + } +} + +} -- cgit v1.3