Initial omemo:1 support

This commit is contained in:
Marvin W 2020-03-29 14:07:14 +02:00
parent cdf7bf26b9
commit 98a1f3ad63
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
13 changed files with 578 additions and 192 deletions

@ -1 +1 @@
Subproject commit 0c5c9561b7e70d8c643460ef2c069255d15a9619
Subproject commit 06184660790daa42433e616fa3dee730717d1c1b

View file

@ -1,3 +1,4 @@
using Omemo;
namespace Omemo {
public class Context {
@ -81,6 +82,12 @@ public class Context {
return res;
}
public SignalMessage deserialize_signal_message_omemo(uint8[] data) throws Error {
SignalMessage res;
throw_by_code(signal_message_deserialize_omemo(out res, data, native_context));
return res;
}
public SignalMessage copy_signal_message(CiphertextMessage original) throws Error {
SignalMessage res;
throw_by_code(signal_message_copy(out res, (SignalMessage) original, native_context));
@ -93,11 +100,27 @@ public class Context {
return res;
}
public PreKeySignalMessage deserialize_pre_key_signal_message_omemo(uint8[] data, int32 sid) throws Error {
PreKeySignalMessage res;
throw_by_code(pre_key_signal_message_deserialize_omemo(out res, data, sid, native_context));
return res;
}
public PreKeySignalMessage copy_pre_key_signal_message(CiphertextMessage original) throws Error {
PreKeySignalMessage res;
throw_by_code(pre_key_signal_message_copy(out res, (PreKeySignalMessage) original, native_context));
return res;
}
public uint8[] derive_payload_secret(uint8[] ikm, size_t output_len) throws Error {
NativeHkdfContext context;
throw_by_code(NativeHkdfContext.create(out context, 4, native_context));
uint8[] empty = new uint8[32];
Memory.set(empty, 0, empty.length);
uint8[] output = null;
context.derive_secrets(out output, ikm, empty, "OMEMO Payload".data, output_len);
return output;
}
}
}

View file

@ -377,9 +377,9 @@ public class Store : Object {
throw_by_code(Protocol.Session.delete_session(native_context, address));
}
public SessionRecord load_session(Address other) throws Error {
public SessionRecord load_session(Address other, int preferred_version = 2) throws Error {
SessionRecord record;
throw_by_code(Protocol.Session.load_session(native_context, out record, other));
throw_by_code(Protocol.Session.load_session(native_context, out record, other, preferred_version));
return record;
}

View file

@ -239,12 +239,16 @@ namespace Omemo {
public static int session_cipher_create(out SessionCipher cipher, NativeStoreContext store, Address remote_address, NativeContext global_context);
[CCode (cname = "pre_key_signal_message_deserialize", cheader_filename = "omemo/protocol.h")]
public static int pre_key_signal_message_deserialize(out PreKeySignalMessage message, uint8[] data, NativeContext global_context);
[CCode (cname = "pre_key_signal_message_deserialize_omemo", cheader_filename = "omemo/protocol.h")]
public static int pre_key_signal_message_deserialize_omemo(out PreKeySignalMessage message, uint8[] data, uint32 remote_registration_id, NativeContext global_context);
[CCode (cname = "pre_key_signal_message_copy", cheader_filename = "omemo/protocol.h")]
public static int pre_key_signal_message_copy(out PreKeySignalMessage message, PreKeySignalMessage other_message, NativeContext global_context);
[CCode (cname = "signal_message_create", cheader_filename = "omemo/protocol.h")]
public static int signal_message_create(out SignalMessage message, uint8 message_version, uint8[] mac_key, ECPublicKey sender_ratchet_key, uint32 counter, uint32 previous_counter, uint8[] ciphertext, ECPublicKey sender_identity_key, ECPublicKey receiver_identity_key, NativeContext global_context);
[CCode (cname = "signal_message_deserialize", cheader_filename = "omemo/protocol.h")]
public static int signal_message_deserialize(out SignalMessage message, uint8[] data, NativeContext global_context);
[CCode (cname = "signal_message_deserialize_omemo", cheader_filename = "omemo/protocol.h")]
public static int signal_message_deserialize_omemo(out SignalMessage message, uint8[] data, NativeContext global_context);
[CCode (cname = "signal_message_copy", cheader_filename = "omemo/protocol.h")]
public static int signal_message_copy(out SignalMessage message, SignalMessage other_message, NativeContext global_context);
[CCode (cname = "curve_generate_key_pair", cheader_filename = "omemo/curve.h")]

View file

@ -98,6 +98,7 @@ namespace Omemo {
public void process_pre_key_bundle(PreKeyBundle pre_key_bundle) throws GLib.Error {
throw_by_code(process_pre_key_bundle_(pre_key_bundle));
}
public int version { get; set; }
}
[Compact]
@ -299,7 +300,9 @@ namespace Omemo {
return res.data;
}
public int get_remote_registration_id(out uint32 remote_id);
public int get_session_version(uint32 version);
public int get_session_version(out uint32 version);
public uint32 version { get; set; }
[CCode (has_target = false)]
public delegate int DecryptionCallback(SessionCipher cipher, Buffer plaintext, void* decrypt_context);

View file

@ -104,7 +104,7 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding {
.put_node(new StanzaNode.build("iv", Omemo.Legacy.NS_URI)
.put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector)))));
plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList<Jid>.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account);
plugin.trust_manager.encrypt_key(header_node, null, security_params.secret.transport_key, null, local_full_jid.bare_jid, new ArrayList<Jid>.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account);
security.put_node(encrypted_node);
}

View file

@ -6,7 +6,7 @@ using Dino.Entities;
namespace Dino.Plugins.Omemo {
public class Database : Qlite.Database {
private const int VERSION = 5;
private const int VERSION = 6;
public class IdentityMetaTable : Table {
//Default to provide backwards compatability
@ -20,10 +20,12 @@ public class Database : Qlite.Database {
public Column<long> last_active = new Column.Long("last_active");
public Column<long> last_message_untrusted = new Column.Long("last_message_untrusted") { min_version = 5 };
public Column<long> last_message_undecryptable = new Column.Long("last_message_undecryptable") { min_version = 5 };
public Column<int> version = new Column.Integer("version") { default = "0", min_version = 6 };
public Column<string?> label = new Column.Text("label") { min_version = 6 };
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});
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, version, label});
index("identity_meta_idx", {identity_id, address_name, device_id}, true);
index("identity_meta_list_idx", {identity_id, address_name});
}
@ -36,8 +38,8 @@ public class Database : Qlite.Database {
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<int32> devices) {
update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform();
public void insert_legacy_device_list(int32 identity_id, string address_name, ArrayList<int32> devices) {
update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).with(this.version, "=", ProtocolVersion.LEGACY.to_int()).set(now_active, false).perform();
foreach (int32 device_id in devices) {
upsert()
.value(this.identity_id, identity_id, true)
@ -49,6 +51,21 @@ public class Database : Qlite.Database {
}
}
public void insert_v1_device_list(int32 identity_id, string address_name, ArrayList<V1.DeviceListItem> devices) {
update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).with(this.version, "=", ProtocolVersion.V1.to_int()).set(now_active, false).perform();
foreach (V1.DeviceListItem 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.device_id, true)
.value(this.now_active, true)
.value(this.last_active, (long) new DateTime.now_utc().to_unix())
.value(this.version, ProtocolVersion.V1.to_int())
.value(this.label, device_id.label)
.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!
@ -66,7 +83,7 @@ public class Database : Qlite.Database {
.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) {
public int64 insert_device_session(int32 identity_id, string address_name, int device_id, string identity_key, ProtocolVersion version, 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.");
@ -77,6 +94,7 @@ public class Database : Qlite.Database {
.value(this.address_name, address_name, true)
.value(this.device_id, device_id, true)
.value(this.identity_key_public_base64, identity_key)
.value(this.version, version.to_int())
.value(this.trust_level, trust).perform();
}

View file

@ -17,7 +17,7 @@ public class EncryptState {
public bool own_list { get; internal set; }
public string to_string() {
return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))";
return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists), own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))";
}
}

View file

@ -148,18 +148,26 @@ public class Manager : StreamInteractionModule, Object {
} else {
debug("delaying message %s", state.to_string());
// TODO: V1 support
if (state.waiting_own_sessions > 0) {
legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid));
var devices = trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid);
var list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold<ArrayList<int32>>((d, list) => { list.add(d.device_id); return list; }, new ArrayList<int32>());
legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, list);
list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold<ArrayList<int32>>((d, list) => { list.add(d.device_id); return list; }, new ArrayList<int32>());
v1_module.fetch_bundles((!)stream, conversation.account.bare_jid, list);
}
if (state.waiting_other_sessions > 0 && message.counterpart != null) {
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
legacy_module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid));
var devices = trust_manager.get_trusted_devices(conversation.account, jid);
var list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold<ArrayList<int32>>((d, list) => { list.add(d.device_id); return list; }, new ArrayList<int32>());
legacy_module.fetch_bundles((!)stream, jid, list);
list = devices.filter((d) => d.version == ProtocolVersion.V1).fold<ArrayList<int32>>((d, list) => { list.add(d.device_id); return list; }, new ArrayList<int32>());
v1_module.fetch_bundles((!)stream, jid, list);
}
}
if (state.waiting_other_devicelists > 0 && message.counterpart != null) {
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
legacy_module.request_user_devicelist.begin((!)stream, jid);
v1_module.request_user_devicelist.begin((!)stream, jid);
}
}
}
@ -179,22 +187,22 @@ public class Manager : StreamInteractionModule, Object {
Legacy.StreamModule legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY);
if (legacy_module != null) {
legacy_module.request_user_devicelist.begin(stream, account.bare_jid);
legacy_module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices));
legacy_module.device_list_loaded.connect((jid, devices) => on_legacy_device_list_loaded(account, jid, devices));
legacy_module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
legacy_module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid));
}
V1.StreamModule v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY);
if (v1_module != null) {
v1_module.request_user_devicelist.begin(stream, account.bare_jid);
//v1_module.device_list_loaded.connect((jid, devices) => on_device_list_loaded_v1(account, jid, devices));
v1_module.device_list_loaded.connect((jid, devices) => on_v1_device_list_loaded(account, jid, devices));
v1_module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
v1_module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid));
}
initialize_store.begin(account);
}
private void on_device_list_loaded(Account account, Jid jid, ArrayList<int32> device_list) {
debug("received device list for %s from %s", account.bare_jid.to_string(), jid.to_string());
private void on_legacy_device_list_loaded(Account account, Jid jid, ArrayList<int32> device_list) {
debug("[Legacy] received device list for %s from %s", account.bare_jid.to_string(), jid.to_string());
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) {
@ -209,7 +217,7 @@ public class Manager : StreamInteractionModule, Object {
if (identity_id < 0) return;
//Update meta database
db.identity_meta.insert_device_list(identity_id, jid.bare_jid.to_string(), device_list);
db.identity_meta.insert_legacy_device_list(identity_id, jid.bare_jid.to_string(), device_list);
//Fetch the bundle for each new device
int inc = 0;
@ -225,6 +233,45 @@ public class Manager : StreamInteractionModule, Object {
debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string());
}
on_after_devicelist_laoded(account, jid, identity_id);
}
private void on_v1_device_list_loaded(Account account, Jid jid, ArrayList<V1.DeviceListItem> device_list) {
debug("[V1] received device list for %s from %s", account.bare_jid.to_string(), jid.to_string());
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) {
return;
}
V1.StreamModule? module = ((!)stream).get_module(V1.StreamModule.IDENTITY);
if (module == null) {
return;
}
int identity_id = db.identity.get_id(account.id);
if (identity_id < 0) return;
//Update meta database
db.identity_meta.insert_v1_device_list(identity_id, jid.bare_jid.to_string(), device_list);
//Fetch the bundle for each new device
int inc = 0;
foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) {
try {
module.fetch_bundle(stream, new Jid(row[db.identity_meta.address_name]), row[db.identity_meta.device_id], false);
inc++;
} catch (InvalidJidError e) {
warning("Ignoring device with invalid Jid: %s", e.message);
}
}
if (inc > 0) {
debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string());
}
on_after_devicelist_laoded(account, jid, identity_id);
}
private void on_after_devicelist_laoded(Account account, Jid jid, int identity_id) {
//Create an entry for the jid in the account table if one does not exist already
if (db.trust.select().with(db.trust.identity_id, "=", identity_id).with(db.trust.address_name, "=", jid.bare_jid.to_string()).count() == 0) {
db.trust.insert().value(db.trust.identity_id, identity_id).value(db.trust.address_name, jid.bare_jid.to_string()).value(db.trust.blind_trust, true).perform();
@ -254,7 +301,6 @@ public class Manager : StreamInteractionModule, Object {
if (conv == null) continue;
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true);
}
}
private void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) {
@ -288,7 +334,7 @@ public class Manager : StreamInteractionModule, Object {
if (should_start_session(account, jid)) {
XmppStream? stream = stream_interactor.get_stream(account);
if (stream != null) {
Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY);
BaseStreamModule? module = get_module_from_stream(stream, bundle.version);
if (module != null) {
module.start_session(stream, jid, device_id, bundle);
}
@ -297,6 +343,14 @@ public class Manager : StreamInteractionModule, Object {
continue_message_sending(account, jid);
}
private BaseStreamModule? get_module_from_stream(XmppStream stream, ProtocolVersion version) {
switch (version) {
case ProtocolVersion.LEGACY: return stream.get_module(Legacy.StreamModule.IDENTITY);
case ProtocolVersion.V1: return stream.get_module(V1.StreamModule.IDENTITY);
}
return null;
}
private bool should_start_session(Account account, Jid jid) {
lock (message_states) {
foreach (Entities.Message msg in message_states.keys) {

View file

@ -6,6 +6,11 @@ using Qlite;
namespace Dino.Plugins.Omemo {
public class TrustedDevice {
public int32 device_id;
public ProtocolVersion version;
}
public class TrustManager {
public signal void bad_message_state_updated(Account account, Jid jid, int device_id);
@ -69,7 +74,7 @@ public class TrustManager {
}
}
private StanzaNode create_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error {
private StanzaNode create_legacy_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error {
SessionCipher cipher = store.create_session_cipher(address);
CiphertextMessage device_key = cipher.encrypt(key);
debug("Created encrypted key for %s/%d", address.name, address.device_id);
@ -80,9 +85,46 @@ public class TrustManager {
return key_node;
}
internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
private void append_v1_encrypted_key_node(StanzaNode v1_header_node, uint8[] key, Address address, Store store) throws GLib.Error {
SessionCipher cipher = store.create_session_cipher(address);
cipher.version = 4;
uint32 version;
cipher.get_session_version(out version);
if (version != 4) {
warning(@"OMEMO:1 not configured: Session version: $(version) != 4 for $(address.name):$(address.device_id)");
throw new Error(-1, ErrorCode.UNKNOWN, "Session is outdated");
}
CiphertextMessage device_key = cipher.encrypt(key);
debug("Created encrypted key for %s/%d", address.name, address.device_id);
StanzaNode key_node = new StanzaNode.build("key", V1.NS_URI)
.put_attribute("rid", address.device_id.to_string())
.put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("kex", "true");
bool matched = false;
foreach (Xmpp.StanzaNode keys in v1_header_node.get_subnodes("keys")) {
if (keys.get_attribute("jid") == address.name) {
keys.put_node(key_node);
matched = true;
break;
}
}
if (!matched) {
v1_header_node.put_node(new StanzaNode.build("keys", V1.NS_URI).put_attribute("jid", address.name).put_node(key_node));
}
}
private BaseStreamModule? pick_module(Legacy.StreamModule? legacy_module, V1.StreamModule? v1_module, ProtocolVersion version) {
switch (version) {
case ProtocolVersion.LEGACY: return legacy_module;
case ProtocolVersion.V1: return v1_module;
}
return null;
}
internal EncryptState encrypt_key(StanzaNode legacy_header_node, StanzaNode v1_header_node, uint8[] legacy_keytag, uint8[] v1_keymac, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
EncryptState status = new EncryptState();
Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY);
Legacy.StreamModule? legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY);
V1.StreamModule? v1_module = stream.get_module(V1.StreamModule.IDENTITY);
//Check we have the bundles and device lists needed to send the message
if (!is_known_address(account, self_jid)) return status;
@ -103,16 +145,25 @@ public class TrustManager {
//Encrypt the key for each recipient's device individually
Address address = new Address("", 0);
foreach (Jid recipient in recipients) {
foreach(int32 device_id in get_trusted_devices(account, recipient)) {
if (module.is_ignored_device(recipient, device_id)) {
foreach(TrustedDevice device in get_trusted_devices(account, recipient)) {
if (pick_module(legacy_module, v1_module, device.version).is_ignored_device(recipient, device.device_id)) {
status.other_lost++;
continue;
}
try {
address.name = recipient.bare_jid.to_string();
address.device_id = (int) device_id;
StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store);
header_node.put_node(key_node);
address.device_id = (int) device.device_id;
switch (device.version) {
case ProtocolVersion.LEGACY:
if (legacy_header_node == null) continue;
StanzaNode key_node = create_legacy_encrypted_key_node(legacy_keytag, address, legacy_module.store);
legacy_header_node.put_node(key_node);
break;
case ProtocolVersion.V1:
if (v1_header_node == null) continue;
append_v1_encrypted_key_node(v1_header_node, v1_keymac, address, v1_module.store);
break;
}
status.other_success++;
} catch (Error e) {
if (e.code == ErrorCode.UNKNOWN) status.other_unknown++;
@ -123,16 +174,25 @@ public class TrustManager {
// Encrypt the key for each own device
address.name = self_jid.bare_jid.to_string();
foreach(int32 device_id in get_trusted_devices(account, self_jid)) {
if (module.is_ignored_device(self_jid, device_id)) {
foreach(TrustedDevice device in get_trusted_devices(account, self_jid)) {
if (pick_module(legacy_module, v1_module, device.version).is_ignored_device(self_jid, device.device_id)) {
status.own_lost++;
continue;
}
if (device_id != module.store.local_registration_id) {
address.device_id = (int) device_id;
if (device.device_id != v1_module.store.local_registration_id) {
address.device_id = (int) device.device_id;
try {
StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store);
header_node.put_node(key_node);
switch (device.version) {
case ProtocolVersion.LEGACY:
if (legacy_header_node == null) continue;
StanzaNode key_node = create_legacy_encrypted_key_node(legacy_keytag, address, legacy_module.store);
legacy_header_node.put_node(key_node);
break;
case ProtocolVersion.V1:
if (v1_header_node == null) continue;
append_v1_encrypted_key_node(v1_header_node, v1_keymac, address, v1_module.store);
break;
}
status.own_success++;
} catch (Error e) {
if (e.code == ErrorCode.UNKNOWN) status.own_unknown++;
@ -144,40 +204,81 @@ public class TrustManager {
return status;
}
private static uint8[] hmac(ChecksumType type, uint8[] key, uint8[] data) {
Hmac hmac = new Hmac(type, key);
hmac.update(data);
uint8[] res = new uint8[type.get_length()];
size_t resl = res.length;
hmac.get_digest(res, ref resl);
return res;
}
public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) {
EncryptState status = new EncryptState();
if (!Plugin.ensure_context()) return status;
if (message.to == null) return status;
Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY);
Legacy.StreamModule legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY);
Legacy.StreamModule v1_module = stream.get_module(Legacy.StreamModule.IDENTITY);
try {
//Create a key and use it to encrypt the message
uint8[] key = new uint8[16];
Plugin.get_context().randomize(key);
uint8[] iv = new uint8[16];
Plugin.get_context().randomize(iv);
//Create legacy key and use it to encrypt the message
uint8[] legacy_key = new uint8[16];
Plugin.get_context().randomize(legacy_key);
uint8[] legacy_iv = new uint8[12];
Plugin.get_context().randomize(legacy_iv);
uint8[] legacy_aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, legacy_key, legacy_iv, message.body.data);
uint8[] legacy_ciphertext = legacy_aes_encrypt_result[0:legacy_aes_encrypt_result.length-16];
uint8[] legacy_tag = legacy_aes_encrypt_result[legacy_aes_encrypt_result.length-16:legacy_aes_encrypt_result.length];
uint8[] legacy_keytag = new uint8[legacy_key.length + legacy_tag.length];
Memory.copy(legacy_keytag, legacy_key, legacy_key.length);
Memory.copy((uint8*)legacy_keytag + legacy_key.length, legacy_tag, legacy_tag.length);
uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16];
uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length];
uint8[] keytag = new uint8[key.length + tag.length];
Memory.copy(keytag, key, key.length);
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
// Same for v1
uint8[] v1_key = new uint8[32];
Plugin.get_context().randomize(v1_key);
uint8[] v1_hkdf = Plugin.get_context().derive_payload_secret(v1_key, 80);
uint8[] v1_enc_key = v1_hkdf[0:32];
uint8[] v1_auth_key = v1_hkdf[32:64];
uint8[] v1_iv = v1_hkdf[64:80];
// TODO: build proper sce message
string v1_sce_content = @"<content xmlns='urn:xmpp:sce:0'><payload><body xmlns='jabber:client'>$(message.body.replace("&", "&amp;").replace("\"", "&quot;").replace("'", "&apos;").replace("<", "&lt;").replace(">", "&gt;"))</body></payload><rpad>nope</rpad></content>";
uint8[] v1_ciphertext = aes_encrypt(Cipher.AES_CBC_PKCS5, v1_enc_key, v1_iv, v1_sce_content.data);
uint8[] v1_hmac = hmac(ChecksumType.SHA256, v1_auth_key, v1_sce_content.data);
uint8[] v1_keymac = new uint8[v1_key.length + v1_hmac.length];
Memory.copy(v1_keymac, v1_key, v1_key.length);
Memory.copy((uint8*)v1_keymac + v1_key.length, v1_hmac, v1_hmac.length);
StanzaNode header_node;
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Legacy.NS_URI).add_self_xmlns()
.put_node(header_node = new StanzaNode.build("header", Legacy.NS_URI)
.put_attribute("sid", module.store.local_registration_id.to_string())
StanzaNode legacy_header_node = null, legacy_encrypted_node = null;
if (legacy_module != null) {
legacy_encrypted_node = new StanzaNode.build("encrypted", Legacy.NS_URI).add_self_xmlns()
.put_node(legacy_header_node = new StanzaNode.build("header", Legacy.NS_URI)
.put_attribute("sid", legacy_module.store.local_registration_id.to_string())
.put_node(new StanzaNode.build("iv", Legacy.NS_URI)
.put_node(new StanzaNode.text(Base64.encode(iv)))))
.put_node(new StanzaNode.text(Base64.encode(legacy_iv)))))
.put_node(new StanzaNode.build("payload", Legacy.NS_URI)
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
.put_node(new StanzaNode.text(Base64.encode(legacy_ciphertext))));
}
status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account);
StanzaNode v1_header_node = null, v1_encrypted_node = null;
if (v1_module != null) {
v1_encrypted_node = new StanzaNode.build("encrypted", V1.NS_URI).add_self_xmlns()
.put_node(v1_header_node = new StanzaNode.build("header", V1.NS_URI)
.put_attribute("sid", v1_module.store.local_registration_id.to_string()))
.put_node(new StanzaNode.build("payload", V1.NS_URI)
.put_node(new StanzaNode.text(Base64.encode(v1_ciphertext))));
}
message.stanza.put_node(encrypted_node);
status = encrypt_key(legacy_header_node, v1_header_node, legacy_keytag, v1_keymac, self_jid, recipients, stream, account);
if (legacy_header_node.get_subnodes("key").size > 0) {
message.stanza.put_node(legacy_encrypted_node);
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO");
}
if (v1_header_node.get_subnodes("keys").size > 0) {
message.stanza.put_node(v1_encrypted_node);
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, V1.NS_URI, "OMEMO");
}
message.body = "[This message is OMEMO encrypted]";
status.encrypted = true;
} catch (Error e) {
@ -194,13 +295,13 @@ public class TrustManager {
return db.identity_meta.with_address(identity_id, jid.to_string()).with(db.identity_meta.last_active, ">", 0).count() > 0;
}
public Gee.List<int32> get_trusted_devices(Account account, Jid jid) {
Gee.List<int32> devices = new ArrayList<int32>();
public Gee.List<TrustedDevice> get_trusted_devices(Account account, Jid jid) {
Gee.List<TrustedDevice> devices = new ArrayList<TrustedDevice>();
int identity_id = db.identity.get_id(account.id);
if (identity_id < 0) return devices;
foreach (Row device in db.identity_meta.get_trusted_devices(identity_id, jid.bare_jid.to_string())) {
if(device[db.identity_meta.trust_level] != TrustLevel.UNKNOWN || device[db.identity_meta.identity_key_public_base64] == null)
devices.add(device[db.identity_meta.device_id]);
devices.add(new TrustedDevice() { device_id = device[db.identity_meta.device_id], version = ProtocolVersion.from_int(device[db.identity_meta.version]) });
}
return devices;
}
@ -277,12 +378,14 @@ public class TrustManager {
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY);
Store store = module.store;
Store store = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY).store;
StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", Legacy.NS_URI);
if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
StanzaNode encrypted = (!)_encrypted;
StanzaNode? legacy_encrypted = stanza.stanza.get_subnode("encrypted", Legacy.NS_URI);
StanzaNode? v1_encrypted = stanza.stanza.get_subnode("encrypted", V1.NS_URI);
if ((legacy_encrypted == null && v1_encrypted == null) || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == V1.NS_URI) {
message.body = "[This message is OMEMO:1 encrypted]"; // TODO temporary
}
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == Legacy.NS_URI) {
message.body = "[This message is OMEMO encrypted]"; // TODO temporary
}
@ -290,28 +393,189 @@ public class TrustManager {
int identity_id = db.identity.get_id(conversation.account.id);
MessageFlag flag = new MessageFlag();
stanza.add_flag(flag);
StanzaNode? _header = encrypted.get_subnode("header");
if (_header == null) return false;
StanzaNode header = (!)_header;
int sid = header.get_attribute_int("sid");
if (sid <= 0) return false;
var our_nodes = new ArrayList<StanzaNode>();
foreach (StanzaNode key_node in header.get_subnodes("key")) {
int sid = -1;
StanzaNode? v1_header = null;
if (v1_encrypted != null) { v1_header = v1_encrypted.get_subnode("header"); }
if (v1_header != null) { sid = v1_header.get_attribute_int("sid"); }
StanzaNode? our_v1_node = null;
if (v1_header != null && sid > 0) {
foreach (StanzaNode keys_node in v1_header.get_subnodes("keys")) {
if (keys_node.get_attribute("jid") == conversation.account.bare_jid.to_string()) {
foreach (StanzaNode key_node in keys_node.get_subnodes("key")) {
debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id);
if (key_node.get_attribute_int("rid") == store.local_registration_id) {
our_nodes.add(key_node);
our_v1_node = key_node;
}
}
}
}
}
if (our_v1_node != null) {
string? payload = v1_encrypted.get_deep_string_content("payload");
string? key_node_content = our_v1_node.get_string_content();
if (key_node_content != null) {
if (yield decrypt_v1_key_node(message, stanza, conversation, our_v1_node, identity_id, sid, payload)) {
return false;
}
}
}
foreach (StanzaNode key_node in our_nodes) {
string? payload = encrypted.get_deep_string_content("payload");
string? iv_node = header.get_deep_string_content("iv");
string? key_node_content = key_node.get_string_content();
if (iv_node == null || key_node_content == null) continue;
uint8[] key;
StanzaNode? legacy_header = null;
if (legacy_encrypted != null) { legacy_header = legacy_encrypted.get_subnode("header"); }
sid = -1;
if (legacy_header != null) { sid = legacy_header.get_attribute_int("sid"); }
var our_legacy_nodes = new ArrayList<StanzaNode>();
if (legacy_header != null && sid > 0) {
foreach (StanzaNode key_node in legacy_header.get_subnodes("key")) {
debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id);
if (key_node.get_attribute_int("rid") == store.local_registration_id) {
our_legacy_nodes.add(key_node);
}
}
}
foreach (StanzaNode key_node in our_legacy_nodes) {
string? payload = legacy_encrypted.get_deep_string_content("payload");
string? iv_node = legacy_header.get_deep_string_content("iv");
if (iv_node == null || key_node.get_string_content() == null) continue;
uint8[] iv = Base64.decode((!)iv_node);
if (decrypt_legacy_key_node(message, stanza, conversation, key_node, identity_id, sid, payload, iv)) {
return false;
}
}
if (our_legacy_nodes.size == 0 && our_v1_node == null) {
db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time);
trust_manager.bad_message_state_updated(conversation.account, message.from, sid);
}
debug("Received OMEMO encryped message that could not be decrypted.");
return false;
}
private async bool decrypt_v1_key_node(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation, StanzaNode key_node, int identity_id, int sid, string? payload) {
V1.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, V1.StreamModule.IDENTITY);
Store store = module.store;
string? key_node_content = key_node.get_string_content();
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
if (conversation.type_ == Conversation.Type.CHAT) {
possible_jids.add(stanza.from.bare_jid);
} else {
Jid? real_jid = message.real_jid;
if (real_jid != null) {
possible_jids.add(real_jid.bare_jid);
} else if (key_node.get_attribute_bool("kex")) {
// pre key messages do store the identity key, so we can use that to find the real jid
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message_omemo(Base64.decode((!)key_node_content), sid);
string identity_key = Base64.encode(msg.identity_key.serialize());
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) {
try {
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
} catch (InvalidJidError e) {
warning("[V1] Ignoring invalid jid from database: %s", e.message);
}
}
if (possible_jids.size != 1) {
return false;
}
} else {
// If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) {
try {
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
} catch (InvalidJidError e) {
warning("[V1] Ignoring invalid jid from database: %s", e.message);
}
}
}
}
uint8[] key;
foreach (Jid possible_jid in possible_jids) {
try {
Address address = new Address(possible_jid.to_string(), sid);
if (key_node.get_attribute_bool("kex")) {
Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid);
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message_omemo(Base64.decode((!)key_node_content), sid);
string identity_key = Base64.encode(msg.identity_key.serialize());
if (device != null && device[db.identity_meta.identity_key_public_base64] != null) {
if (device[db.identity_meta.identity_key_public_base64] != identity_key) {
critical("[V1] Tried to use a different identity key for a known device id.");
continue;
}
} else {
debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid);
bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true);
if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, ProtocolVersion.V1, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
critical("[V1] Failed learning a device.");
continue;
}
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (device == null && stream != null) {
module.request_user_devicelist.begin(stream, possible_jid);
}
}
debug("[V1] Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid);
SessionCipher cipher = store.create_session_cipher(address);
cipher.version = 4;
key = cipher.decrypt_pre_key_signal_message(msg);
// TODO: Finish session
} else {
debug("[V1] Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid);
SignalMessage msg = Plugin.get_context().deserialize_signal_message_omemo(Base64.decode((!)key_node_content));
SessionCipher cipher = store.create_session_cipher(address);
cipher.version = 4;
key = cipher.decrypt_signal_message(msg);
}
//address.device_id = 0; // TODO: Hack to have address obj live longer
if (key.length != 64) {
critical("[V1] Key length is invalid.");
continue;
}
if (payload != null) {
uint8[] ciphertext = Base64.decode(payload);
uint8[] ikm = key[0:32];
uint8[] mac = key[32:64];
uint8[] hkdf = Plugin.get_context().derive_payload_secret(ikm, 80);
uint8[] enc_key = hkdf[0:32];
uint8[] auth_key = hkdf[32:64];
uint8[] iv = hkdf[64:80];
uint8[] decrypted = aes_decrypt(Cipher.AES_CBC_PKCS5, enc_key, iv, ciphertext);
uint8[] mac_cmp = hmac(ChecksumType.SHA256, auth_key, decrypted);
if (Memory.cmp(mac_cmp, mac, mac.length) != 0) {
critical("[V1] HMAC mismatches.");
continue;
}
// TODO: SCE
StanzaNode content = yield new StanzaReader.for_buffer(decrypted).read_stanza_node();
message.body = content.get_subnode("payload").get_subnode("body", "jabber:client").get_string_content();
message_device_id_map[message] = address.device_id;
message.encryption = Encryption.OMEMO;
}
MessageFlag.get_flag(stanza).decrypted = true;
} catch (Error e) {
debug("[V1] Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
continue;
}
// If we figured out which real jid a message comes from due to decryption working, save it
if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
message.real_jid = possible_jid;
}
return true;
}
return false;
}
private bool decrypt_legacy_key_node(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation, StanzaNode key_node, int identity_id, int sid, string? payload, uint8[] iv) {
Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY);
Store store = module.store;
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
string? key_node_content = key_node.get_string_content();
if (conversation.type_ == Conversation.Type.CHAT) {
possible_jids.add(stanza.from.bare_jid);
} else {
@ -326,11 +590,11 @@ public class TrustManager {
try {
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
} catch (InvalidJidError e) {
warning("Ignoring invalid jid from database: %s", e.message);
warning("[Legacy] Ignoring invalid jid from database: %s", e.message);
}
}
if (possible_jids.size != 1) {
continue;
return false;
}
} else {
// If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id
@ -338,7 +602,7 @@ public class TrustManager {
try {
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
} catch (InvalidJidError e) {
warning("Ignoring invalid jid from database: %s", e.message);
warning("[Legacy] Ignoring invalid jid from database: %s", e.message);
}
}
}
@ -348,6 +612,8 @@ public class TrustManager {
debug("Received message from unknown entity with device id %d", sid);
}
uint8[] key;
foreach (Jid possible_jid in possible_jids) {
try {
Address address = new Address(possible_jid.to_string(), sid);
@ -357,14 +623,14 @@ public class TrustManager {
string identity_key = Base64.encode(msg.identity_key.serialize());
if (device != null && device[db.identity_meta.identity_key_public_base64] != null) {
if (device[db.identity_meta.identity_key_public_base64] != identity_key) {
critical("Tried to use a different identity key for a known device id.");
critical("[Legacy] Tried to use a different identity key for a known device id.");
continue;
}
} else {
debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid);
bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true);
if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
critical("Failed learning a device.");
if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, ProtocolVersion.LEGACY, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
critical("[Legacy] Failed learning a device.");
continue;
}
XmppStream? stream = stream_interactor.get_stream(conversation.account);
@ -372,12 +638,12 @@ public class TrustManager {
module.request_user_devicelist.begin(stream, possible_jid);
}
}
debug("Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid);
debug("[Legacy] Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid);
SessionCipher cipher = store.create_session_cipher(address);
key = cipher.decrypt_pre_key_signal_message(msg);
// TODO: Finish session
} else {
debug("Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid);
debug("[Legacy] Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid);
SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content));
SessionCipher cipher = store.create_session_cipher(address);
key = cipher.decrypt_signal_message(msg);
@ -385,7 +651,7 @@ public class TrustManager {
//address.device_id = 0; // TODO: Hack to have address obj live longer
if (payload != null) {
uint8[] ciphertext = Base64.decode(payload ?? "");
uint8[] ciphertext = Base64.decode(payload);
if (key.length >= 32) {
int authtaglength = key.length - 16;
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
@ -401,9 +667,9 @@ public class TrustManager {
message_device_id_map[message] = address.device_id;
message.encryption = Encryption.OMEMO;
}
flag.decrypted = true;
MessageFlag.get_flag(stanza).decrypted = true;
} catch (Error e) {
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
debug("[Legacy] Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
continue;
}
@ -411,16 +677,8 @@ public class TrustManager {
if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
message.real_jid = possible_jid;
}
return false;
return true;
}
}
if (our_nodes.size == 0) {
db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time);
trust_manager.bad_message_state_updated(conversation.account, message.from, sid);
}
debug("Received OMEMO encryped message that could not be decrypted.");
return false;
}

View file

@ -1,4 +1,6 @@
namespace Dino.Plugins.Omemo {
public interface BaseStreamModule {
}
using Xmpp;
public interface Dino.Plugins.Omemo.BaseStreamModule : Object {
public abstract bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle);
public abstract bool is_ignored_device(Jid jid, int32 device_id);
}

View file

@ -100,7 +100,12 @@ public class StreamModule : XmppStreamModule, BaseStreamModule {
if (!is_ignored_device(jid, device_id)) {
address.device_id = device_id;
try {
if (!store.contains_session(address)) {
if (store.contains_session(address)) {
var session = store.load_session(address, 4);
if (session.state.session_version != 4) {
fetch_bundle(stream, jid, device_id);
}
} else {
fetch_bundle(stream, jid, device_id);
}
} catch (Error e) {
@ -186,10 +191,15 @@ public class StreamModule : XmppStreamModule, BaseStreamModule {
Address address = new Address(jid.bare_jid.to_string(), device_id);
try {
if (store.contains_session(address)) {
var session = store.load_session(address, 4);
if (session.state.session_version == 4) {
return false;
}
store.delete_session(address);
}
debug("[V1] Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id);
SessionBuilder builder = store.create_session_builder(address);
builder.version = 4;
builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key));
} catch (Error e) {
debug("[V1] Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message);

View file

@ -1,7 +1,21 @@
namespace Dino.Plugins.Omemo {
public enum ProtocolVersion {
public enum Dino.Plugins.Omemo.ProtocolVersion {
UNKNOWN,
LEGACY,
V1
V1;
public static ProtocolVersion from_int(int i) {
switch (i) {
case 0: return LEGACY;
case 1: return V1;
}
return UNKNOWN;
}
public int to_int() {
switch (this) {
case LEGACY: return 0;
case V1: return 1;
}
return -1;
}
}