Initial omemo:1 support
This commit is contained in:
parent
cdf7bf26b9
commit
98a1f3ad63
|
@ -1 +1 @@
|
|||
Subproject commit 0c5c9561b7e70d8c643460ef2c069255d15a9619
|
||||
Subproject commit 06184660790daa42433e616fa3dee730717d1c1b
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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))";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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("&", "&").replace("\"", """).replace("'", "'").replace("<", "<").replace(">", ">"))</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())
|
||||
.put_node(new StanzaNode.build("iv", Legacy.NS_URI)
|
||||
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
||||
.put_node(new StanzaNode.build("payload", Legacy.NS_URI)
|
||||
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
||||
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(legacy_iv)))))
|
||||
.put_node(new StanzaNode.build("payload", Legacy.NS_URI)
|
||||
.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);
|
||||
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO");
|
||||
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,132 +393,60 @@ 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")) {
|
||||
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);
|
||||
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_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);
|
||||
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("prekey")) {
|
||||
// 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(Base64.decode((!)key_node_content));
|
||||
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("Ignoring invalid jid from database: %s", e.message);
|
||||
}
|
||||
}
|
||||
if (possible_jids.size != 1) {
|
||||
continue;
|
||||
}
|
||||
} 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("Ignoring invalid jid from database: %s", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (possible_jids.size == 0) {
|
||||
debug("Received message from unknown entity with device id %d", sid);
|
||||
}
|
||||
|
||||
foreach (Jid possible_jid in possible_jids) {
|
||||
try {
|
||||
Address address = new Address(possible_jid.to_string(), sid);
|
||||
if (key_node.get_attribute_bool("prekey")) {
|
||||
Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid);
|
||||
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
||||
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.");
|
||||
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.");
|
||||
continue;
|
||||
}
|
||||
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||
if (device == null && stream != null) {
|
||||
module.request_user_devicelist.begin(stream, possible_jid);
|
||||
}
|
||||
}
|
||||
debug("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);
|
||||
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);
|
||||
}
|
||||
//address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||
|
||||
if (payload != null) {
|
||||
uint8[] ciphertext = Base64.decode(payload ?? "");
|
||||
if (key.length >= 32) {
|
||||
int authtaglength = key.length - 16;
|
||||
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
||||
uint8[] new_key = new uint8[16];
|
||||
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
||||
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
||||
Memory.copy(new_key, key, 16);
|
||||
ciphertext = new_ciphertext;
|
||||
key = new_key;
|
||||
}
|
||||
|
||||
message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
|
||||
message_device_id_map[message] = address.device_id;
|
||||
message.encryption = Encryption.OMEMO;
|
||||
}
|
||||
flag.decrypted = true;
|
||||
} catch (Error e) {
|
||||
debug("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;
|
||||
}
|
||||
if (decrypt_legacy_key_node(message, stanza, conversation, key_node, identity_id, sid, payload, iv)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (our_nodes.size == 0) {
|
||||
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);
|
||||
}
|
||||
|
@ -424,6 +455,233 @@ public class TrustManager {
|
|||
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 {
|
||||
Jid? real_jid = message.real_jid;
|
||||
if (real_jid != null) {
|
||||
possible_jids.add(real_jid.bare_jid);
|
||||
} else if (key_node.get_attribute_bool("prekey")) {
|
||||
// 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(Base64.decode((!)key_node_content));
|
||||
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("[Legacy] 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("[Legacy] Ignoring invalid jid from database: %s", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (possible_jids.size == 0) {
|
||||
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);
|
||||
if (key_node.get_attribute_bool("prekey")) {
|
||||
Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid);
|
||||
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
||||
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("[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, ProtocolVersion.LEGACY, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
|
||||
critical("[Legacy] 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("[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("[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);
|
||||
}
|
||||
//address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||
|
||||
if (payload != null) {
|
||||
uint8[] ciphertext = Base64.decode(payload);
|
||||
if (key.length >= 32) {
|
||||
int authtaglength = key.length - 16;
|
||||
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
||||
uint8[] new_key = new uint8[16];
|
||||
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
||||
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
||||
Memory.copy(new_key, key, 16);
|
||||
ciphertext = new_ciphertext;
|
||||
key = new_key;
|
||||
}
|
||||
|
||||
message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
|
||||
message_device_id_map[message] = address.device_id;
|
||||
message.encryption = Encryption.OMEMO;
|
||||
}
|
||||
MessageFlag.get_flag(stanza).decrypted = true;
|
||||
} catch (Error e) {
|
||||
debug("[Legacy] 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 string arr_to_str(uint8[] arr) {
|
||||
// null-terminate the array
|
||||
uint8[] rarr = new uint8[arr.length+1];
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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)) {
|
||||
return false;
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue