Start announcing omemo:1 devices/bundles
This commit is contained in:
parent
615e383786
commit
cdf7bf26b9
|
@ -213,6 +213,8 @@ namespace Omemo {
|
||||||
}
|
}
|
||||||
return buffer.data;
|
return buffer.data;
|
||||||
}
|
}
|
||||||
|
public Buffer ed { get; }
|
||||||
|
public Buffer mont { get; }
|
||||||
public int compare(ECPublicKey other);
|
public int compare(ECPublicKey other);
|
||||||
public int memcmp(ECPublicKey other);
|
public int memcmp(ECPublicKey other);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ SOURCES
|
||||||
src/protocol/bundle.vala
|
src/protocol/bundle.vala
|
||||||
src/protocol/message_flag.vala
|
src/protocol/message_flag.vala
|
||||||
src/protocol/stream_module.vala
|
src/protocol/stream_module.vala
|
||||||
|
src/protocol/legacy_stream_module.vala
|
||||||
|
src/protocol/v1_stream_module.vala
|
||||||
|
src/protocol/version.vala
|
||||||
|
|
||||||
src/ui/account_settings_entry.vala
|
src/ui/account_settings_entry.vala
|
||||||
src/ui/account_settings_widget.vala
|
src/ui/account_settings_widget.vala
|
||||||
|
|
|
@ -37,14 +37,14 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
}
|
}
|
||||||
|
|
||||||
public string get_type_uri() {
|
public string get_type_uri() {
|
||||||
return Omemo.NS_URI;
|
return Omemo.Legacy.NS_URI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError {
|
public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError {
|
||||||
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
Store store = stream.get_module(Omemo.Legacy.StreamModule.IDENTITY).store;
|
||||||
StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI);
|
StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.Legacy.NS_URI);
|
||||||
if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element");
|
if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element");
|
||||||
StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI);
|
StanzaNode? header = encrypted.get_subnode("header", Omemo.Legacy.NS_URI);
|
||||||
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element");
|
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element");
|
||||||
string? iv_node = header.get_deep_string_content("iv");
|
string? iv_node = header.get_deep_string_content("iv");
|
||||||
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element");
|
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element");
|
||||||
|
@ -84,7 +84,7 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
|
|
||||||
public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) {
|
public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) {
|
||||||
ArrayList<Account> accounts = plugin.app.stream_interactor.get_accounts();
|
ArrayList<Account> accounts = plugin.app.stream_interactor.get_accounts();
|
||||||
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
Store store = stream.get_module(Omemo.Legacy.StreamModule.IDENTITY).store;
|
||||||
Account? account = null;
|
Account? account = null;
|
||||||
foreach (Account compare in accounts) {
|
foreach (Account compare in accounts) {
|
||||||
if (compare.bare_jid.equals_bare(local_full_jid)) {
|
if (compare.bare_jid.equals_bare(local_full_jid)) {
|
||||||
|
@ -98,10 +98,10 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
}
|
}
|
||||||
|
|
||||||
StanzaNode header_node;
|
StanzaNode header_node;
|
||||||
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns()
|
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.Legacy.NS_URI).add_self_xmlns()
|
||||||
.put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI)
|
.put_node(header_node = new StanzaNode.build("header", Omemo.Legacy.NS_URI)
|
||||||
.put_attribute("sid", store.local_registration_id.to_string())
|
.put_attribute("sid", store.local_registration_id.to_string())
|
||||||
.put_node(new StanzaNode.build("iv", Omemo.NS_URI)
|
.put_node(new StanzaNode.build("iv", Omemo.Legacy.NS_URI)
|
||||||
.put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector)))));
|
.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, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList<Jid>.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account);
|
||||||
|
|
|
@ -39,12 +39,12 @@ public class EncryptionHelper : JingleFileEncryptionHelper, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) {
|
public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) {
|
||||||
return new Xep.Jet.Options(Omemo.NS_URI, AES_128_GCM_URI);
|
return new Xep.Jet.Options(Omemo.Legacy.NS_URI, AES_128_GCM_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) {
|
public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) {
|
||||||
Xep.Jet.SecurityParameters? security = jingle_transfer.security as Xep.Jet.SecurityParameters;
|
Xep.Jet.SecurityParameters? security = jingle_transfer.security as Xep.Jet.SecurityParameters;
|
||||||
if (security != null && security.encoding.get_type_uri() == Omemo.NS_URI) {
|
if (security != null && security.encoding.get_type_uri() == Omemo.Legacy.NS_URI) {
|
||||||
file_transfer.encryption = Encryption.OMEMO;
|
file_transfer.encryption = Encryption.OMEMO;
|
||||||
}
|
}
|
||||||
return file_meta;
|
return file_meta;
|
||||||
|
|
|
@ -73,7 +73,8 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
if (stream == null) return;
|
if (stream == null) return;
|
||||||
|
|
||||||
stream.get_module(StreamModule.IDENTITY).clear_device_list(stream);
|
stream.get_module(Legacy.StreamModule.IDENTITY).clear_device_list(stream);
|
||||||
|
stream.get_module(V1.StreamModule.IDENTITY).clear_device_list(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Gee.List<Jid> get_occupants(Jid jid, Account account){
|
private Gee.List<Jid> get_occupants(Jid jid, Account account){
|
||||||
|
@ -100,12 +101,12 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
message.marked = Entities.Message.Marked.UNSENT;
|
message.marked = Entities.Message.Marked.UNSENT;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StreamModule? module_ = ((!)stream).get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule? legacy_module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY);
|
||||||
if (module_ == null) {
|
V1.StreamModule? v1_module = ((!)stream).get_module(V1.StreamModule.IDENTITY);
|
||||||
|
if (legacy_module == null && v1_module == null) {
|
||||||
message.marked = Entities.Message.Marked.UNSENT;
|
message.marked = Entities.Message.Marked.UNSENT;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StreamModule module = (!)module_;
|
|
||||||
|
|
||||||
//Get a list of everyone for whom the message should be encrypted
|
//Get a list of everyone for whom the message should be encrypted
|
||||||
Gee.List<Jid> recipients;
|
Gee.List<Jid> recipients;
|
||||||
|
@ -147,17 +148,18 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
} else {
|
} else {
|
||||||
debug("delaying message %s", state.to_string());
|
debug("delaying message %s", state.to_string());
|
||||||
|
|
||||||
|
// TODO: V1 support
|
||||||
if (state.waiting_own_sessions > 0) {
|
if (state.waiting_own_sessions > 0) {
|
||||||
module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid));
|
legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid));
|
||||||
}
|
}
|
||||||
if (state.waiting_other_sessions > 0 && message.counterpart != null) {
|
if (state.waiting_other_sessions > 0 && message.counterpart != null) {
|
||||||
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
|
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
|
||||||
module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid));
|
legacy_module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.waiting_other_devicelists > 0 && message.counterpart != null) {
|
if (state.waiting_other_devicelists > 0 && message.counterpart != null) {
|
||||||
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
|
foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) {
|
||||||
module.request_user_devicelist.begin((!)stream, jid);
|
legacy_module.request_user_devicelist.begin((!)stream, jid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,16 +171,24 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
if(stream == null) return;
|
if(stream == null) return;
|
||||||
|
|
||||||
stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid);
|
stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid);
|
||||||
|
stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_stream_negotiated(Account account, XmppStream stream) {
|
private void on_stream_negotiated(Account account, XmppStream stream) {
|
||||||
StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY);
|
Legacy.StreamModule legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY);
|
||||||
if (module != null) {
|
if (legacy_module != null) {
|
||||||
module.request_user_devicelist.begin(stream, account.bare_jid);
|
legacy_module.request_user_devicelist.begin(stream, account.bare_jid);
|
||||||
module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices));
|
legacy_module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices));
|
||||||
module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
|
legacy_module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
|
||||||
module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid));
|
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.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);
|
initialize_store.begin(account);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +200,7 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY);
|
||||||
if (module == null) {
|
if (module == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +288,7 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
if (should_start_session(account, jid)) {
|
if (should_start_session(account, jid)) {
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY);
|
||||||
if (module != null) {
|
if (module != null) {
|
||||||
module.start_session(stream, jid, device_id, bundle);
|
module.start_session(stream, jid, device_id, bundle);
|
||||||
}
|
}
|
||||||
|
@ -335,9 +345,20 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
account.notify["id"].connect(() => initialize_store.callback());
|
account.notify["id"].connect(() => initialize_store.callback());
|
||||||
yield;
|
yield;
|
||||||
}
|
}
|
||||||
StreamModule? module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY);
|
Store store = null;
|
||||||
if (module == null) return;
|
Legacy.StreamModule? legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY);
|
||||||
Store store = module.store;
|
V1.StreamModule? v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY);
|
||||||
|
if (legacy_module != null) {
|
||||||
|
store = legacy_module.store;
|
||||||
|
}
|
||||||
|
if (v1_module != null) {
|
||||||
|
if (store != null) {
|
||||||
|
v1_module.store = store;
|
||||||
|
} else {
|
||||||
|
store = v1_module.store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (store == null) return;
|
||||||
Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner;
|
Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner;
|
||||||
int identity_id = -1;
|
int identity_id = -1;
|
||||||
bool publish_identity = false;
|
bool publish_identity = false;
|
||||||
|
@ -379,7 +400,12 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
// Generated new device ID, ensure this gets added to the devicelist
|
// Generated new device ID, ensure this gets added to the devicelist
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
module.request_user_devicelist.begin((!)stream, account.bare_jid);
|
if (legacy_module != null) {
|
||||||
|
legacy_module.request_user_devicelist.begin((!)stream, account.bare_jid);
|
||||||
|
}
|
||||||
|
if (v1_module != null) {
|
||||||
|
v1_module.request_user_devicelist.begin((!)stream, account.bare_jid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,8 +427,15 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
if (trust_manager.is_known_address(account, jid)) return true;
|
if (trust_manager.is_known_address(account, jid)) return true;
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
var device_list = yield stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist(stream, jid);
|
var legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY);
|
||||||
return device_list.size > 0;
|
if (legacy_module != null) {
|
||||||
|
if ((yield legacy_module.request_user_devicelist(stream, jid)).size > 0) return true;
|
||||||
|
}
|
||||||
|
var v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY);
|
||||||
|
if (legacy_module != null) {
|
||||||
|
if ((yield v1_module.request_user_devicelist(stream, jid)).size > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true; // TODO wait for stream?
|
return true; // TODO wait for stream?
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class TrustManager {
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
CiphertextMessage device_key = cipher.encrypt(key);
|
CiphertextMessage device_key = cipher.encrypt(key);
|
||||||
debug("Created encrypted key for %s/%d", address.name, address.device_id);
|
debug("Created encrypted key for %s/%d", address.name, address.device_id);
|
||||||
StanzaNode key_node = new StanzaNode.build("key", NS_URI)
|
StanzaNode key_node = new StanzaNode.build("key", Legacy.NS_URI)
|
||||||
.put_attribute("rid", address.device_id.to_string())
|
.put_attribute("rid", address.device_id.to_string())
|
||||||
.put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
|
.put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
|
||||||
if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true");
|
if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true");
|
||||||
|
@ -82,7 +82,7 @@ public class TrustManager {
|
||||||
|
|
||||||
internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
|
internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
|
||||||
EncryptState status = new EncryptState();
|
EncryptState status = new EncryptState();
|
||||||
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY);
|
||||||
|
|
||||||
//Check we have the bundles and device lists needed to send the message
|
//Check we have the bundles and device lists needed to send the message
|
||||||
if (!is_known_address(account, self_jid)) return status;
|
if (!is_known_address(account, self_jid)) return status;
|
||||||
|
@ -149,7 +149,7 @@ public class TrustManager {
|
||||||
if (!Plugin.ensure_context()) return status;
|
if (!Plugin.ensure_context()) return status;
|
||||||
if (message.to == null) return status;
|
if (message.to == null) return status;
|
||||||
|
|
||||||
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//Create a key and use it to encrypt the message
|
//Create a key and use it to encrypt the message
|
||||||
|
@ -166,18 +166,18 @@ public class TrustManager {
|
||||||
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
||||||
|
|
||||||
StanzaNode header_node;
|
StanzaNode header_node;
|
||||||
StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
|
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Legacy.NS_URI).add_self_xmlns()
|
||||||
.put_node(header_node = new StanzaNode.build("header", NS_URI)
|
.put_node(header_node = new StanzaNode.build("header", Legacy.NS_URI)
|
||||||
.put_attribute("sid", module.store.local_registration_id.to_string())
|
.put_attribute("sid", module.store.local_registration_id.to_string())
|
||||||
.put_node(new StanzaNode.build("iv", NS_URI)
|
.put_node(new StanzaNode.build("iv", Legacy.NS_URI)
|
||||||
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
||||||
.put_node(new StanzaNode.build("payload", NS_URI)
|
.put_node(new StanzaNode.build("payload", Legacy.NS_URI)
|
||||||
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
||||||
|
|
||||||
status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account);
|
status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account);
|
||||||
|
|
||||||
message.stanza.put_node(encrypted_node);
|
message.stanza.put_node(encrypted_node);
|
||||||
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO");
|
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO");
|
||||||
message.body = "[This message is OMEMO encrypted]";
|
message.body = "[This message is OMEMO encrypted]";
|
||||||
status.encrypted = true;
|
status.encrypted = true;
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
|
@ -277,12 +277,15 @@ public class TrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY);
|
Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY);
|
||||||
Store store = module.store;
|
Store store = module.store;
|
||||||
|
|
||||||
StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI);
|
StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", Legacy.NS_URI);
|
||||||
if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
|
if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
|
||||||
StanzaNode encrypted = (!)_encrypted;
|
StanzaNode encrypted = (!)_encrypted;
|
||||||
|
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == Legacy.NS_URI) {
|
||||||
|
message.body = "[This message is OMEMO encrypted]"; // TODO temporary
|
||||||
|
}
|
||||||
if (!Plugin.ensure_context()) return false;
|
if (!Plugin.ensure_context()) return false;
|
||||||
int identity_id = db.identity.get_id(conversation.account.id);
|
int identity_id = db.identity.get_id(conversation.account.id);
|
||||||
MessageFlag flag = new MessageFlag();
|
MessageFlag flag = new MessageFlag();
|
||||||
|
@ -305,9 +308,8 @@ public class TrustManager {
|
||||||
string? payload = encrypted.get_deep_string_content("payload");
|
string? payload = encrypted.get_deep_string_content("payload");
|
||||||
string? iv_node = header.get_deep_string_content("iv");
|
string? iv_node = header.get_deep_string_content("iv");
|
||||||
string? key_node_content = key_node.get_string_content();
|
string? key_node_content = key_node.get_string_content();
|
||||||
if (payload == null || iv_node == null || key_node_content == null) continue;
|
if (iv_node == null || key_node_content == null) continue;
|
||||||
uint8[] key;
|
uint8[] key;
|
||||||
uint8[] ciphertext = Base64.decode((!)payload);
|
|
||||||
uint8[] iv = Base64.decode((!)iv_node);
|
uint8[] iv = Base64.decode((!)iv_node);
|
||||||
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
|
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
|
||||||
if (conversation.type_ == Conversation.Type.CHAT) {
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
@ -382,20 +384,23 @@ public class TrustManager {
|
||||||
}
|
}
|
||||||
//address.device_id = 0; // TODO: Hack to have address obj live longer
|
//address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
|
||||||
if (key.length >= 32) {
|
if (payload != null) {
|
||||||
int authtaglength = key.length - 16;
|
uint8[] ciphertext = Base64.decode(payload ?? "");
|
||||||
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
if (key.length >= 32) {
|
||||||
uint8[] new_key = new uint8[16];
|
int authtaglength = key.length - 16;
|
||||||
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
||||||
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
uint8[] new_key = new uint8[16];
|
||||||
Memory.copy(new_key, key, 16);
|
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
||||||
ciphertext = new_ciphertext;
|
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
||||||
key = new_key;
|
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.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
|
||||||
message_device_id_map[message] = address.device_id;
|
message_device_id_map[message] = address.device_id;
|
||||||
message.encryption = Encryption.OMEMO;
|
message.encryption = Encryption.OMEMO;
|
||||||
|
}
|
||||||
flag.decrypted = true;
|
flag.decrypted = true;
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
|
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
|
||||||
|
|
|
@ -50,7 +50,8 @@ public class Plugin : RootInterface, Object {
|
||||||
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
||||||
this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this));
|
this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this));
|
||||||
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
||||||
list.add(new StreamModule());
|
list.add(new Legacy.StreamModule());
|
||||||
|
list.add(new V1.StreamModule());
|
||||||
list.add(new JetOmemo.Module(this));
|
list.add(new JetOmemo.Module(this));
|
||||||
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,16 +12,40 @@ public class Bundle {
|
||||||
assert(Plugin.ensure_context());
|
assert(Plugin.ensure_context());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProtocolVersion version { get {
|
||||||
|
switch(node.ns_uri) {
|
||||||
|
case Legacy.NS_URI: return ProtocolVersion.LEGACY;
|
||||||
|
case V1.NS_URI: return ProtocolVersion.V1;
|
||||||
|
}
|
||||||
|
return ProtocolVersion.UNKNOWN;
|
||||||
|
}}
|
||||||
|
|
||||||
public int32 signed_pre_key_id { owned get {
|
public int32 signed_pre_key_id { owned get {
|
||||||
if (node == null) return -1;
|
if (node == null) return -1;
|
||||||
string? id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId");
|
string? id = null;
|
||||||
|
switch(version) {
|
||||||
|
case ProtocolVersion.LEGACY:
|
||||||
|
id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId");
|
||||||
|
break;
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
id = ((!)node).get_deep_attribute("spk", "id");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (id == null) return -1;
|
if (id == null) return -1;
|
||||||
return int.parse((!)id);
|
return int.parse((!)id);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
public ECPublicKey? signed_pre_key { owned get {
|
public ECPublicKey? signed_pre_key { owned get {
|
||||||
if (node == null) return null;
|
if (node == null) return null;
|
||||||
string? key = ((!)node).get_deep_string_content("signedPreKeyPublic");
|
string? key = null;
|
||||||
|
switch(version) {
|
||||||
|
case ProtocolVersion.LEGACY:
|
||||||
|
key = ((!)node).get_deep_string_content("signedPreKeyPublic");
|
||||||
|
break;
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
key = ((!)node).get_deep_string_content("spk");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
try {
|
try {
|
||||||
return Plugin.get_context().decode_public_key(Base64.decode((!)key));
|
return Plugin.get_context().decode_public_key(Base64.decode((!)key));
|
||||||
|
@ -32,14 +56,30 @@ public class Bundle {
|
||||||
|
|
||||||
public uint8[]? signed_pre_key_signature { owned get {
|
public uint8[]? signed_pre_key_signature { owned get {
|
||||||
if (node == null) return null;
|
if (node == null) return null;
|
||||||
string? sig = ((!)node).get_deep_string_content("signedPreKeySignature");
|
string? sig = null;
|
||||||
|
switch(version) {
|
||||||
|
case ProtocolVersion.LEGACY:
|
||||||
|
sig = ((!)node).get_deep_string_content("signedPreKeySignature");
|
||||||
|
break;
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
sig = ((!)node).get_deep_string_content("spks");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (sig == null) return null;
|
if (sig == null) return null;
|
||||||
return Base64.decode((!)sig);
|
return Base64.decode((!)sig);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
public ECPublicKey? identity_key { owned get {
|
public ECPublicKey? identity_key { owned get {
|
||||||
if (node == null) return null;
|
if (node == null) return null;
|
||||||
string? key = ((!)node).get_deep_string_content("identityKey");
|
string? key = null;
|
||||||
|
switch(version) {
|
||||||
|
case ProtocolVersion.LEGACY:
|
||||||
|
key = ((!)node).get_deep_string_content("identityKey");
|
||||||
|
break;
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
key = ((!)node).get_deep_string_content("ik");
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
try {
|
try {
|
||||||
return Plugin.get_context().decode_public_key(Base64.decode((!)key));
|
return Plugin.get_context().decode_public_key(Base64.decode((!)key));
|
||||||
|
@ -51,10 +91,21 @@ public class Bundle {
|
||||||
public ArrayList<PreKey> pre_keys { owned get {
|
public ArrayList<PreKey> pre_keys { owned get {
|
||||||
ArrayList<PreKey> list = new ArrayList<PreKey>();
|
ArrayList<PreKey> list = new ArrayList<PreKey>();
|
||||||
if (node == null || ((!)node).get_subnode("prekeys") == null) return list;
|
if (node == null || ((!)node).get_subnode("prekeys") == null) return list;
|
||||||
((!)node).get_deep_subnodes("prekeys", "preKeyPublic")
|
|
||||||
.filter((node) => ((!)node).get_attribute("preKeyId") != null)
|
switch(version) {
|
||||||
.map<PreKey>(PreKey.create)
|
case ProtocolVersion.LEGACY:
|
||||||
.foreach((key) => list.add(key));
|
((!)node).get_deep_subnodes("prekeys", "preKeyPublic")
|
||||||
|
.filter((node) => ((!)node).get_attribute("preKeyId") != null)
|
||||||
|
.map<PreKey>(PreKey.create)
|
||||||
|
.foreach((key) => list.add(key));
|
||||||
|
break;
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
((!)node).get_deep_subnodes("prekeys", "pk")
|
||||||
|
.filter((node) => ((!)node).get_attribute("id") != null)
|
||||||
|
.map<PreKey>(PreKey.create)
|
||||||
|
.foreach((key) => list.add(key));
|
||||||
|
break;
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -65,12 +116,26 @@ public class Bundle {
|
||||||
return new PreKey(node);
|
return new PreKey(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProtocolVersion version { get {
|
||||||
|
switch(node.ns_uri) {
|
||||||
|
case Legacy.NS_URI: return ProtocolVersion.LEGACY;
|
||||||
|
case V1.NS_URI: return ProtocolVersion.V1;
|
||||||
|
}
|
||||||
|
return ProtocolVersion.UNKNOWN;
|
||||||
|
}}
|
||||||
|
|
||||||
public PreKey(StanzaNode node) {
|
public PreKey(StanzaNode node) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int32 key_id { owned get {
|
public int32 key_id { owned get {
|
||||||
return int.parse(node.get_attribute("preKeyId") ?? "-1");
|
switch(version) {
|
||||||
|
case ProtocolVersion.LEGACY:
|
||||||
|
return int.parse(node.get_attribute("preKeyId") ?? "-1");
|
||||||
|
case ProtocolVersion.V1:
|
||||||
|
return int.parse(node.get_attribute("id") ?? "-1");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
public ECPublicKey? key { owned get {
|
public ECPublicKey? key { owned get {
|
||||||
|
|
327
plugins/omemo/src/protocol/legacy_stream_module.vala
Normal file
327
plugins/omemo/src/protocol/legacy_stream_module.vala
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
using Gee;
|
||||||
|
using Omemo;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo.Legacy {
|
||||||
|
|
||||||
|
private const string NS_URI = "eu.siacs.conversations.axolotl";
|
||||||
|
private const string NODE_DEVICELIST = NS_URI + ".devicelist";
|
||||||
|
private const string NODE_BUNDLES = NS_URI + ".bundles";
|
||||||
|
private const string NODE_VERIFICATION = NS_URI + ".verification";
|
||||||
|
|
||||||
|
private const int NUM_KEYS_TO_PUBLISH = 100;
|
||||||
|
|
||||||
|
public class StreamModule : XmppStreamModule, BaseStreamModule {
|
||||||
|
public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module");
|
||||||
|
private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE;
|
||||||
|
|
||||||
|
public Store store { public get; private set; }
|
||||||
|
private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>();
|
||||||
|
private HashMap<Jid, Future<ArrayList<int32>>> active_devicelist_requests = new HashMap<Jid, Future<ArrayList<int32>>>(Jid.hash_func, Jid.equals_func);
|
||||||
|
private Map<string, DateTime> device_ignore_time = new HashMap<string, DateTime>();
|
||||||
|
|
||||||
|
public signal void device_list_loaded(Jid jid, ArrayList<int32> devices);
|
||||||
|
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
|
||||||
|
public signal void bundle_fetch_failed(Jid jid, int device_id);
|
||||||
|
|
||||||
|
public StreamModule() {
|
||||||
|
if (Plugin.ensure_context()) {
|
||||||
|
this.store = Plugin.get_context().create_store();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {}
|
||||||
|
|
||||||
|
public async ArrayList<int32> request_user_devicelist(XmppStream stream, Jid jid) {
|
||||||
|
var future = active_devicelist_requests[jid];
|
||||||
|
if (future == null) {
|
||||||
|
var promise = new Promise<ArrayList<int32>?>();
|
||||||
|
future = promise.future;
|
||||||
|
active_devicelist_requests[jid] = future;
|
||||||
|
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => {
|
||||||
|
ArrayList<int32> device_list = parse_device_list(stream, jid, id, node);
|
||||||
|
promise.set_value(device_list);
|
||||||
|
active_devicelist_requests.unset(jid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ArrayList<int32> device_list = yield future.wait_async();
|
||||||
|
return device_list;
|
||||||
|
} catch (FutureError error) {
|
||||||
|
warning("[Legacy] Future error when waiting for device list: %s", error.message);
|
||||||
|
return new ArrayList<int32>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<int32> parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) {
|
||||||
|
ArrayList<int32> device_list = new ArrayList<int32>();
|
||||||
|
|
||||||
|
StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns();
|
||||||
|
Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
|
||||||
|
if (my_jid == null) return device_list;
|
||||||
|
if (jid.equals_bare(my_jid) && store.local_registration_id != 0) {
|
||||||
|
bool am_on_devicelist = false;
|
||||||
|
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
||||||
|
int device_id = device_node.get_attribute_int("id");
|
||||||
|
if (store.local_registration_id == device_id) {
|
||||||
|
am_on_devicelist = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!am_on_devicelist) {
|
||||||
|
debug("[Legacy] Not on device list, adding id");
|
||||||
|
node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string()));
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node);
|
||||||
|
}
|
||||||
|
publish_bundles_if_needed(stream, jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
||||||
|
device_list.add(device_node.get_attribute_int("id"));
|
||||||
|
}
|
||||||
|
device_list_loaded(jid, device_list);
|
||||||
|
|
||||||
|
return device_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetch_bundles(XmppStream stream, Jid jid, Gee.List<int32> devices) {
|
||||||
|
Address address = new Address(jid.bare_jid.to_string(), 0);
|
||||||
|
foreach(int32 device_id in devices) {
|
||||||
|
if (!is_ignored_device(jid, device_id)) {
|
||||||
|
address.device_id = device_id;
|
||||||
|
try {
|
||||||
|
if (!store.contains_session(address)) {
|
||||||
|
fetch_bundle(stream, jid, device_id);
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) {
|
||||||
|
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) {
|
||||||
|
debug("[Legacy] Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id);
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => {
|
||||||
|
on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ignore_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unignore_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
device_ignore_time.unset(jid.bare_jid.to_string() + @":$device_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool is_ignored_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return true;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
string id = jid.bare_jid.to_string() + @":$device_id";
|
||||||
|
if (device_ignore_time.has_key(id)) {
|
||||||
|
return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear_device_list(XmppStream stream) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) {
|
||||||
|
if (node == null) {
|
||||||
|
// Device not registered, shouldn't exist
|
||||||
|
if (ignore_if_non_present) {
|
||||||
|
debug("[Legacy] Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id);
|
||||||
|
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||||
|
}
|
||||||
|
bundle_fetch_failed(jid, device_id);
|
||||||
|
} else {
|
||||||
|
Bundle bundle = new Bundle(node);
|
||||||
|
stream.get_module(IDENTITY).unignore_device(jid, device_id);
|
||||||
|
debug("[Legacy] Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize()));
|
||||||
|
bundle_fetched(jid, device_id, bundle);
|
||||||
|
}
|
||||||
|
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) {
|
||||||
|
bool fail = false;
|
||||||
|
int32 signed_pre_key_id = bundle.signed_pre_key_id;
|
||||||
|
ECPublicKey? signed_pre_key = bundle.signed_pre_key;
|
||||||
|
uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
|
||||||
|
ECPublicKey? identity_key = bundle.identity_key;
|
||||||
|
|
||||||
|
ArrayList<Bundle.PreKey> pre_keys = bundle.pre_keys;
|
||||||
|
if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) {
|
||||||
|
fail = true;
|
||||||
|
} else {
|
||||||
|
int pre_key_idx = Random.int_range(0, pre_keys.size);
|
||||||
|
int32 pre_key_id = pre_keys[pre_key_idx].key_id;
|
||||||
|
ECPublicKey? pre_key = pre_keys[pre_key_idx].key;
|
||||||
|
if (pre_key_id < 0 || pre_key == null) {
|
||||||
|
fail = true;
|
||||||
|
} else {
|
||||||
|
Address address = new Address(jid.bare_jid.to_string(), device_id);
|
||||||
|
try {
|
||||||
|
if (store.contains_session(address)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
debug("[Legacy] Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id);
|
||||||
|
SessionBuilder builder = store.create_session_builder(address);
|
||||||
|
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("[Legacy] Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message);
|
||||||
|
fail = true;
|
||||||
|
}
|
||||||
|
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fail) {
|
||||||
|
debug("[Legacy] Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string());
|
||||||
|
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publish_bundles_if_needed(XmppStream stream, Jid jid) {
|
||||||
|
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) {
|
||||||
|
if (!Plugin.ensure_context()) return;
|
||||||
|
Map<int, ECPublicKey> keys = new HashMap<int, ECPublicKey>();
|
||||||
|
ECPublicKey? identity_key = null;
|
||||||
|
int32 signed_pre_key_id = -1;
|
||||||
|
ECPublicKey? signed_pre_key = null;
|
||||||
|
SignedPreKeyRecord? signed_pre_key_record = null;
|
||||||
|
bool changed = false;
|
||||||
|
if (node == null) {
|
||||||
|
identity_key = store.identity_key_pair.public;
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
Bundle bundle = new Bundle(node);
|
||||||
|
foreach (Bundle.PreKey prekey in bundle.pre_keys) {
|
||||||
|
ECPublicKey? key = prekey.key;
|
||||||
|
if (key != null) {
|
||||||
|
keys[prekey.key_id] = (!)key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
identity_key = bundle.identity_key;
|
||||||
|
signed_pre_key_id = bundle.signed_pre_key_id;
|
||||||
|
signed_pre_key = bundle.signed_pre_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate IdentityKey
|
||||||
|
if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
IdentityKeyPair identity_key_pair = store.identity_key_pair;
|
||||||
|
|
||||||
|
// Validate signedPreKeyRecord + ID
|
||||||
|
if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) {
|
||||||
|
signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
||||||
|
signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id);
|
||||||
|
store.store_signed_pre_key((!)signed_pre_key_record);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate PreKeys
|
||||||
|
Set<PreKeyRecord> pre_key_records = new HashSet<PreKeyRecord>();
|
||||||
|
foreach (var entry in keys.entries) {
|
||||||
|
if (store.contains_pre_key(entry.key)) {
|
||||||
|
PreKeyRecord record = store.load_pre_key(entry.key);
|
||||||
|
if (record.key_pair.public.compare(entry.value) == 0) {
|
||||||
|
pre_key_records.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size;
|
||||||
|
if (new_keys > 0) {
|
||||||
|
int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
||||||
|
Set<PreKeyRecord> new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys);
|
||||||
|
pre_key_records.add_all(new_records);
|
||||||
|
foreach (PreKeyRecord record in new_records) {
|
||||||
|
store.store_pre_key(record);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id);
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
warning(@"[Legacy] Unexpected error while publishing bundle: $(e.message)\n");
|
||||||
|
}
|
||||||
|
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set<PreKeyRecord> pre_key_records, int32 device_id) throws Error {
|
||||||
|
ECKeyPair tmp;
|
||||||
|
StanzaNode bundle = new StanzaNode.build("bundle", NS_URI)
|
||||||
|
.add_self_xmlns()
|
||||||
|
.put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI)
|
||||||
|
.put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string())
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize()))))
|
||||||
|
.put_node(new StanzaNode.build("signedPreKeySignature", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature))))
|
||||||
|
.put_node(new StanzaNode.build("identityKey", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize()))));
|
||||||
|
StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI);
|
||||||
|
foreach (PreKeyRecord pre_key_record in pre_key_records) {
|
||||||
|
prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI)
|
||||||
|
.put_attribute("preKeyId", pre_key_record.id.to_string())
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize()))));
|
||||||
|
}
|
||||||
|
bundle.put_node(prekeys);
|
||||||
|
|
||||||
|
yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", "1", bundle);
|
||||||
|
yield try_make_bundle_public(stream, device_id);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void try_make_bundle_public(XmppStream stream, int32 device_id) {
|
||||||
|
DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, @"$NODE_BUNDLES:$device_id");
|
||||||
|
if (data_form == null) return;
|
||||||
|
|
||||||
|
foreach (DataForms.DataForm.Field field in data_form.fields) {
|
||||||
|
if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) {
|
||||||
|
field.set_value_string(Pubsub.ACCESS_MODEL_OPEN);
|
||||||
|
yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, @"$NODE_BUNDLES:$device_id");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() {
|
||||||
|
return NS_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_id() {
|
||||||
|
return IDENTITY.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,11 +8,11 @@ public class MessageFlag : Xmpp.MessageFlag {
|
||||||
public bool decrypted = false;
|
public bool decrypted = false;
|
||||||
|
|
||||||
public static MessageFlag? get_flag(MessageStanza message) {
|
public static MessageFlag? get_flag(MessageStanza message) {
|
||||||
return (MessageFlag) message.get_flag(NS_URI, id);
|
return (MessageFlag) message.get_flag(V1.NS_URI, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string get_ns() {
|
public override string get_ns() {
|
||||||
return NS_URI;
|
return V1.NS_URI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string get_id() {
|
public override string get_id() {
|
||||||
|
|
|
@ -1,327 +1,4 @@
|
||||||
using Gee;
|
|
||||||
using Omemo;
|
|
||||||
using Xmpp;
|
|
||||||
using Xmpp.Xep;
|
|
||||||
|
|
||||||
namespace Dino.Plugins.Omemo {
|
namespace Dino.Plugins.Omemo {
|
||||||
|
public interface BaseStreamModule {
|
||||||
private const string NS_URI = "eu.siacs.conversations.axolotl";
|
|
||||||
private const string NODE_DEVICELIST = NS_URI + ".devicelist";
|
|
||||||
private const string NODE_BUNDLES = NS_URI + ".bundles";
|
|
||||||
private const string NODE_VERIFICATION = NS_URI + ".verification";
|
|
||||||
|
|
||||||
private const int NUM_KEYS_TO_PUBLISH = 100;
|
|
||||||
|
|
||||||
public class StreamModule : XmppStreamModule {
|
|
||||||
public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module");
|
|
||||||
private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE;
|
|
||||||
|
|
||||||
public Store store { public get; private set; }
|
|
||||||
private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>();
|
|
||||||
private HashMap<Jid, Future<ArrayList<int32>>> active_devicelist_requests = new HashMap<Jid, Future<ArrayList<int32>>>(Jid.hash_func, Jid.equals_func);
|
|
||||||
private Map<string, DateTime> device_ignore_time = new HashMap<string, DateTime>();
|
|
||||||
|
|
||||||
public signal void device_list_loaded(Jid jid, ArrayList<int32> devices);
|
|
||||||
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
|
|
||||||
public signal void bundle_fetch_failed(Jid jid, int device_id);
|
|
||||||
|
|
||||||
public StreamModule() {
|
|
||||||
if (Plugin.ensure_context()) {
|
|
||||||
this.store = Plugin.get_context().create_store();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void attach(XmppStream stream) {
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void detach(XmppStream stream) {}
|
|
||||||
|
|
||||||
public async ArrayList<int32> request_user_devicelist(XmppStream stream, Jid jid) {
|
|
||||||
var future = active_devicelist_requests[jid];
|
|
||||||
if (future == null) {
|
|
||||||
var promise = new Promise<ArrayList<int32>?>();
|
|
||||||
future = promise.future;
|
|
||||||
active_devicelist_requests[jid] = future;
|
|
||||||
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => {
|
|
||||||
ArrayList<int32> device_list = parse_device_list(stream, jid, id, node);
|
|
||||||
promise.set_value(device_list);
|
|
||||||
active_devicelist_requests.unset(jid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ArrayList<int32> device_list = yield future.wait_async();
|
|
||||||
return device_list;
|
|
||||||
} catch (FutureError error) {
|
|
||||||
warning("Future error when waiting for device list: %s", error.message);
|
|
||||||
return new ArrayList<int32>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayList<int32> parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) {
|
|
||||||
ArrayList<int32> device_list = new ArrayList<int32>();
|
|
||||||
|
|
||||||
StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns();
|
|
||||||
Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
|
|
||||||
if (my_jid == null) return device_list;
|
|
||||||
if (jid.equals_bare(my_jid) && store.local_registration_id != 0) {
|
|
||||||
bool am_on_devicelist = false;
|
|
||||||
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
|
||||||
int device_id = device_node.get_attribute_int("id");
|
|
||||||
if (store.local_registration_id == device_id) {
|
|
||||||
am_on_devicelist = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!am_on_devicelist) {
|
|
||||||
debug("Not on device list, adding id");
|
|
||||||
node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string()));
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node);
|
|
||||||
}
|
|
||||||
publish_bundles_if_needed(stream, jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
|
||||||
device_list.add(device_node.get_attribute_int("id"));
|
|
||||||
}
|
|
||||||
device_list_loaded(jid, device_list);
|
|
||||||
|
|
||||||
return device_list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fetch_bundles(XmppStream stream, Jid jid, Gee.List<int32> devices) {
|
|
||||||
Address address = new Address(jid.bare_jid.to_string(), 0);
|
|
||||||
foreach(int32 device_id in devices) {
|
|
||||||
if (!is_ignored_device(jid, device_id)) {
|
|
||||||
address.device_id = device_id;
|
|
||||||
try {
|
|
||||||
if (!store.contains_session(address)) {
|
|
||||||
fetch_bundle(stream, jid, device_id);
|
|
||||||
}
|
|
||||||
} catch (Error e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
address.device_id = 0; // TODO: Hack to have address obj live longer
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) {
|
|
||||||
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) {
|
|
||||||
debug("Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id);
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => {
|
|
||||||
on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ignore_device(Jid jid, int32 device_id) {
|
|
||||||
if (device_id <= 0) return;
|
|
||||||
lock (device_ignore_time) {
|
|
||||||
device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unignore_device(Jid jid, int32 device_id) {
|
|
||||||
if (device_id <= 0) return;
|
|
||||||
lock (device_ignore_time) {
|
|
||||||
device_ignore_time.unset(jid.bare_jid.to_string() + @":$device_id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool is_ignored_device(Jid jid, int32 device_id) {
|
|
||||||
if (device_id <= 0) return true;
|
|
||||||
lock (device_ignore_time) {
|
|
||||||
string id = jid.bare_jid.to_string() + @":$device_id";
|
|
||||||
if (device_ignore_time.has_key(id)) {
|
|
||||||
return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear_device_list(XmppStream stream) {
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) {
|
|
||||||
if (node == null) {
|
|
||||||
// Device not registered, shouldn't exist
|
|
||||||
if (ignore_if_non_present) {
|
|
||||||
debug("Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id);
|
|
||||||
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
|
||||||
}
|
|
||||||
bundle_fetch_failed(jid, device_id);
|
|
||||||
} else {
|
|
||||||
Bundle bundle = new Bundle(node);
|
|
||||||
stream.get_module(IDENTITY).unignore_device(jid, device_id);
|
|
||||||
debug("Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize()));
|
|
||||||
bundle_fetched(jid, device_id, bundle);
|
|
||||||
}
|
|
||||||
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) {
|
|
||||||
bool fail = false;
|
|
||||||
int32 signed_pre_key_id = bundle.signed_pre_key_id;
|
|
||||||
ECPublicKey? signed_pre_key = bundle.signed_pre_key;
|
|
||||||
uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
|
|
||||||
ECPublicKey? identity_key = bundle.identity_key;
|
|
||||||
|
|
||||||
ArrayList<Bundle.PreKey> pre_keys = bundle.pre_keys;
|
|
||||||
if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) {
|
|
||||||
fail = true;
|
|
||||||
} else {
|
|
||||||
int pre_key_idx = Random.int_range(0, pre_keys.size);
|
|
||||||
int32 pre_key_id = pre_keys[pre_key_idx].key_id;
|
|
||||||
ECPublicKey? pre_key = pre_keys[pre_key_idx].key;
|
|
||||||
if (pre_key_id < 0 || pre_key == null) {
|
|
||||||
fail = true;
|
|
||||||
} else {
|
|
||||||
Address address = new Address(jid.bare_jid.to_string(), device_id);
|
|
||||||
try {
|
|
||||||
if (store.contains_session(address)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
debug("Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id);
|
|
||||||
SessionBuilder builder = store.create_session_builder(address);
|
|
||||||
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("Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message);
|
|
||||||
fail = true;
|
|
||||||
}
|
|
||||||
address.device_id = 0; // TODO: Hack to have address obj live longer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fail) {
|
|
||||||
debug("Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string());
|
|
||||||
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void publish_bundles_if_needed(XmppStream stream, Jid jid) {
|
|
||||||
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) {
|
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) {
|
|
||||||
if (!Plugin.ensure_context()) return;
|
|
||||||
Map<int, ECPublicKey> keys = new HashMap<int, ECPublicKey>();
|
|
||||||
ECPublicKey? identity_key = null;
|
|
||||||
int32 signed_pre_key_id = -1;
|
|
||||||
ECPublicKey? signed_pre_key = null;
|
|
||||||
SignedPreKeyRecord? signed_pre_key_record = null;
|
|
||||||
bool changed = false;
|
|
||||||
if (node == null) {
|
|
||||||
identity_key = store.identity_key_pair.public;
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
Bundle bundle = new Bundle(node);
|
|
||||||
foreach (Bundle.PreKey prekey in bundle.pre_keys) {
|
|
||||||
ECPublicKey? key = prekey.key;
|
|
||||||
if (key != null) {
|
|
||||||
keys[prekey.key_id] = (!)key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
identity_key = bundle.identity_key;
|
|
||||||
signed_pre_key_id = bundle.signed_pre_key_id;
|
|
||||||
signed_pre_key = bundle.signed_pre_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate IdentityKey
|
|
||||||
if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) {
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
IdentityKeyPair identity_key_pair = store.identity_key_pair;
|
|
||||||
|
|
||||||
// Validate signedPreKeyRecord + ID
|
|
||||||
if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) {
|
|
||||||
signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
|
||||||
signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id);
|
|
||||||
store.store_signed_pre_key((!)signed_pre_key_record);
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate PreKeys
|
|
||||||
Set<PreKeyRecord> pre_key_records = new HashSet<PreKeyRecord>();
|
|
||||||
foreach (var entry in keys.entries) {
|
|
||||||
if (store.contains_pre_key(entry.key)) {
|
|
||||||
PreKeyRecord record = store.load_pre_key(entry.key);
|
|
||||||
if (record.key_pair.public.compare(entry.value) == 0) {
|
|
||||||
pre_key_records.add(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size;
|
|
||||||
if (new_keys > 0) {
|
|
||||||
int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
|
||||||
Set<PreKeyRecord> new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys);
|
|
||||||
pre_key_records.add_all(new_records);
|
|
||||||
foreach (PreKeyRecord record in new_records) {
|
|
||||||
store.store_pre_key(record);
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id);
|
|
||||||
}
|
|
||||||
} catch (Error e) {
|
|
||||||
warning(@"Unexpected error while publishing bundle: $(e.message)\n");
|
|
||||||
}
|
|
||||||
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set<PreKeyRecord> pre_key_records, int32 device_id) throws Error {
|
|
||||||
ECKeyPair tmp;
|
|
||||||
StanzaNode bundle = new StanzaNode.build("bundle", NS_URI)
|
|
||||||
.add_self_xmlns()
|
|
||||||
.put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI)
|
|
||||||
.put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string())
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize()))))
|
|
||||||
.put_node(new StanzaNode.build("signedPreKeySignature", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature))))
|
|
||||||
.put_node(new StanzaNode.build("identityKey", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize()))));
|
|
||||||
StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI);
|
|
||||||
foreach (PreKeyRecord pre_key_record in pre_key_records) {
|
|
||||||
prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI)
|
|
||||||
.put_attribute("preKeyId", pre_key_record.id.to_string())
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize()))));
|
|
||||||
}
|
|
||||||
bundle.put_node(prekeys);
|
|
||||||
|
|
||||||
yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", "1", bundle);
|
|
||||||
yield try_make_bundle_public(stream, device_id);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void try_make_bundle_public(XmppStream stream, int32 device_id) {
|
|
||||||
DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, @"$NODE_BUNDLES:$device_id");
|
|
||||||
if (data_form == null) return;
|
|
||||||
|
|
||||||
foreach (DataForms.DataForm.Field field in data_form.fields) {
|
|
||||||
if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) {
|
|
||||||
field.set_value_string(Pubsub.ACCESS_MODEL_OPEN);
|
|
||||||
yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, @"$NODE_BUNDLES:$device_id");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string get_ns() {
|
|
||||||
return NS_URI;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string get_id() {
|
|
||||||
return IDENTITY.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
330
plugins/omemo/src/protocol/v1_stream_module.vala
Normal file
330
plugins/omemo/src/protocol/v1_stream_module.vala
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
using Gee;
|
||||||
|
using Omemo;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo.V1 {
|
||||||
|
|
||||||
|
private const string NS_URI = "urn:xmpp:omemo:1";
|
||||||
|
private const string NODE_DEVICELIST = NS_URI + ":devices";
|
||||||
|
private const string NODE_BUNDLES = NS_URI + ":bundles";
|
||||||
|
|
||||||
|
private const int NUM_KEYS_TO_PUBLISH = 100;
|
||||||
|
|
||||||
|
public class DeviceListItem {
|
||||||
|
public int32 device_id;
|
||||||
|
public string label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StreamModule : XmppStreamModule, BaseStreamModule {
|
||||||
|
public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module");
|
||||||
|
private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE;
|
||||||
|
|
||||||
|
public Store store { public get; internal set; }
|
||||||
|
private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>();
|
||||||
|
private HashMap<Jid, Future<ArrayList<DeviceListItem>>> active_devicelist_requests = new HashMap<Jid, Future<ArrayList<DeviceListItem>>>(Jid.hash_func, Jid.equals_func);
|
||||||
|
private Map<string, DateTime> device_ignore_time = new HashMap<string, DateTime>();
|
||||||
|
|
||||||
|
public signal void device_list_loaded(Jid jid, ArrayList<DeviceListItem> devices);
|
||||||
|
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
|
||||||
|
public signal void bundle_fetch_failed(Jid jid, int device_id);
|
||||||
|
|
||||||
|
public StreamModule() {
|
||||||
|
if (Plugin.ensure_context()) {
|
||||||
|
this.store = Plugin.get_context().create_store();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {}
|
||||||
|
|
||||||
|
public async ArrayList<DeviceListItem> request_user_devicelist(XmppStream stream, Jid jid) {
|
||||||
|
var future = active_devicelist_requests[jid];
|
||||||
|
if (future == null) {
|
||||||
|
var promise = new Promise<ArrayList<DeviceListItem>?>();
|
||||||
|
future = promise.future;
|
||||||
|
active_devicelist_requests[jid] = future;
|
||||||
|
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => {
|
||||||
|
ArrayList<DeviceListItem> device_list = parse_device_list(stream, jid, id, node);
|
||||||
|
promise.set_value(device_list);
|
||||||
|
active_devicelist_requests.unset(jid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ArrayList<DeviceListItem> device_list = yield future.wait_async();
|
||||||
|
return device_list;
|
||||||
|
} catch (FutureError error) {
|
||||||
|
warning("[V1] Future error when waiting for device list: %s", error.message);
|
||||||
|
return new ArrayList<DeviceListItem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<DeviceListItem> parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) {
|
||||||
|
ArrayList<DeviceListItem> device_list = new ArrayList<DeviceListItem>();
|
||||||
|
|
||||||
|
StanzaNode node = node_ ?? new StanzaNode.build("devices", NS_URI).add_self_xmlns();
|
||||||
|
Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
|
||||||
|
if (my_jid == null) return device_list;
|
||||||
|
if (jid.equals_bare(my_jid) && store.local_registration_id != 0) {
|
||||||
|
bool am_on_devicelist = false;
|
||||||
|
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
||||||
|
int device_id = device_node.get_attribute_int("id");
|
||||||
|
if (store.local_registration_id == device_id) {
|
||||||
|
am_on_devicelist = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!am_on_devicelist) {
|
||||||
|
debug("[V1] Not on device list, adding id");
|
||||||
|
node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string()));
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node);
|
||||||
|
}
|
||||||
|
publish_bundles_if_needed(stream, jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (StanzaNode device_node in node.get_subnodes("device")) {
|
||||||
|
device_list.add(new DeviceListItem() { device_id = device_node.get_attribute_int("id"), label = device_node.get_attribute("label") });
|
||||||
|
}
|
||||||
|
device_list_loaded(jid, device_list);
|
||||||
|
|
||||||
|
return device_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetch_bundles(XmppStream stream, Jid jid, Gee.List<int32> devices) {
|
||||||
|
Address address = new Address(jid.bare_jid.to_string(), 0);
|
||||||
|
foreach(int32 device_id in devices) {
|
||||||
|
if (!is_ignored_device(jid, device_id)) {
|
||||||
|
address.device_id = device_id;
|
||||||
|
try {
|
||||||
|
if (!store.contains_session(address)) {
|
||||||
|
fetch_bundle(stream, jid, device_id);
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) {
|
||||||
|
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) {
|
||||||
|
debug("[V1] Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id);
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, NODE_BUNDLES, (stream, jid, id, node) => {
|
||||||
|
on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present);
|
||||||
|
}, device_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ignore_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unignore_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
device_ignore_time.unset(@"$(jid.bare_jid):$device_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool is_ignored_device(Jid jid, int32 device_id) {
|
||||||
|
if (device_id <= 0) return true;
|
||||||
|
lock (device_ignore_time) {
|
||||||
|
string id = @"$(jid.bare_jid):$device_id";
|
||||||
|
if (device_ignore_time.has_key(id)) {
|
||||||
|
return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear_device_list(XmppStream stream) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) {
|
||||||
|
if (node == null) {
|
||||||
|
// Device not registered, shouldn't exist
|
||||||
|
if (ignore_if_non_present) {
|
||||||
|
debug("[V1] Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id);
|
||||||
|
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||||
|
}
|
||||||
|
bundle_fetch_failed(jid, device_id);
|
||||||
|
} else {
|
||||||
|
Bundle bundle = new Bundle(node);
|
||||||
|
stream.get_module(IDENTITY).unignore_device(jid, device_id);
|
||||||
|
debug("[V1] Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize()));
|
||||||
|
bundle_fetched(jid, device_id, bundle);
|
||||||
|
}
|
||||||
|
stream.get_module(IDENTITY).active_bundle_requests.remove(@"$(jid.bare_jid):$device_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) {
|
||||||
|
bool fail = false;
|
||||||
|
int32 signed_pre_key_id = bundle.signed_pre_key_id;
|
||||||
|
ECPublicKey? signed_pre_key = bundle.signed_pre_key;
|
||||||
|
uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
|
||||||
|
ECPublicKey? identity_key = bundle.identity_key;
|
||||||
|
|
||||||
|
ArrayList<Bundle.PreKey> pre_keys = bundle.pre_keys;
|
||||||
|
if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) {
|
||||||
|
fail = true;
|
||||||
|
} else {
|
||||||
|
int pre_key_idx = Random.int_range(0, pre_keys.size);
|
||||||
|
int32 pre_key_id = pre_keys[pre_key_idx].key_id;
|
||||||
|
ECPublicKey? pre_key = pre_keys[pre_key_idx].key;
|
||||||
|
if (pre_key_id < 0 || pre_key == null) {
|
||||||
|
fail = true;
|
||||||
|
} else {
|
||||||
|
Address address = new Address(jid.bare_jid.to_string(), device_id);
|
||||||
|
try {
|
||||||
|
if (store.contains_session(address)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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.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);
|
||||||
|
fail = true;
|
||||||
|
}
|
||||||
|
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fail) {
|
||||||
|
debug("[V1] Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string());
|
||||||
|
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publish_bundles_if_needed(XmppStream stream, Jid jid) {
|
||||||
|
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) {
|
||||||
|
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_BUNDLES, on_self_bundle_result, store.local_registration_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) {
|
||||||
|
if (!Plugin.ensure_context()) return;
|
||||||
|
Map<int, ECPublicKey> keys = new HashMap<int, ECPublicKey>();
|
||||||
|
ECPublicKey? identity_key = null;
|
||||||
|
int32 signed_pre_key_id = -1;
|
||||||
|
ECPublicKey? signed_pre_key = null;
|
||||||
|
SignedPreKeyRecord? signed_pre_key_record = null;
|
||||||
|
bool changed = false;
|
||||||
|
if (node == null) {
|
||||||
|
identity_key = store.identity_key_pair.public;
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
Bundle bundle = new Bundle(node);
|
||||||
|
foreach (Bundle.PreKey prekey in bundle.pre_keys) {
|
||||||
|
ECPublicKey? key = prekey.key;
|
||||||
|
if (key != null) {
|
||||||
|
keys[prekey.key_id] = (!)key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
identity_key = bundle.identity_key;
|
||||||
|
signed_pre_key_id = bundle.signed_pre_key_id;
|
||||||
|
signed_pre_key = bundle.signed_pre_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate IdentityKey
|
||||||
|
if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
IdentityKeyPair identity_key_pair = store.identity_key_pair;
|
||||||
|
|
||||||
|
// Validate signedPreKeyRecord + ID
|
||||||
|
if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) {
|
||||||
|
signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
||||||
|
signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id);
|
||||||
|
store.store_signed_pre_key((!)signed_pre_key_record);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate PreKeys
|
||||||
|
Set<PreKeyRecord> pre_key_records = new HashSet<PreKeyRecord>();
|
||||||
|
foreach (var entry in keys.entries) {
|
||||||
|
if (store.contains_pre_key(entry.key)) {
|
||||||
|
PreKeyRecord record = store.load_pre_key(entry.key);
|
||||||
|
if (record.key_pair.public.compare(entry.value) == 0) {
|
||||||
|
pre_key_records.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size;
|
||||||
|
if (new_keys > 0) {
|
||||||
|
int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
|
||||||
|
Set<PreKeyRecord> new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys);
|
||||||
|
pre_key_records.add_all(new_records);
|
||||||
|
foreach (PreKeyRecord record in new_records) {
|
||||||
|
store.store_pre_key(record);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id);
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
warning(@"[V1] Unexpected error while publishing bundle: $(e.message)\n");
|
||||||
|
}
|
||||||
|
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set<PreKeyRecord> pre_key_records, int32 device_id) throws Error {
|
||||||
|
ECKeyPair tmp;
|
||||||
|
StanzaNode bundle = new StanzaNode.build("bundle", NS_URI)
|
||||||
|
.add_self_xmlns()
|
||||||
|
.put_node(new StanzaNode.build("spk", NS_URI)
|
||||||
|
.put_attribute("id", signed_pre_key_record.id.to_string())
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize()))))
|
||||||
|
.put_node(new StanzaNode.build("spks", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature))))
|
||||||
|
.put_node(new StanzaNode.build("ik", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.ed.data))));
|
||||||
|
StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI);
|
||||||
|
foreach (PreKeyRecord pre_key_record in pre_key_records) {
|
||||||
|
prekeys.put_node(new StanzaNode.build("pk", NS_URI)
|
||||||
|
.put_attribute("id", pre_key_record.id.to_string())
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize()))));
|
||||||
|
}
|
||||||
|
bundle.put_node(prekeys);
|
||||||
|
|
||||||
|
yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, NODE_BUNDLES, device_id.to_string(), bundle);
|
||||||
|
yield try_make_bundle_public(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void try_make_bundle_public(XmppStream stream) {
|
||||||
|
DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, NODE_BUNDLES);
|
||||||
|
if (data_form == null) return;
|
||||||
|
|
||||||
|
foreach (DataForms.DataForm.Field field in data_form.fields) {
|
||||||
|
if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) {
|
||||||
|
field.set_value_string(Pubsub.ACCESS_MODEL_OPEN);
|
||||||
|
yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, NODE_BUNDLES);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() {
|
||||||
|
return NS_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_id() {
|
||||||
|
return IDENTITY.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
plugins/omemo/src/protocol/version.vala
Normal file
7
plugins/omemo/src/protocol/version.vala
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Dino.Plugins.Omemo {
|
||||||
|
public enum ProtocolVersion {
|
||||||
|
UNKNOWN,
|
||||||
|
LEGACY,
|
||||||
|
V1
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,15 @@ public class ContactDetailsDialog : Gtk.Dialog {
|
||||||
if (identity_id < 0) return;
|
if (identity_id < 0) return;
|
||||||
Dino.Application? app = Application.get_default() as Dino.Application;
|
Dino.Application? app = Application.get_default() as Dino.Application;
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
store = app.stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store;
|
var legacy_module = app.stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY);
|
||||||
|
var v1_module = app.stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY);
|
||||||
|
if (legacy_module != null) {
|
||||||
|
store = legacy_module.store;
|
||||||
|
} else if (v1_module != null) {
|
||||||
|
store = v1_module.store;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true));
|
auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true));
|
||||||
|
@ -135,22 +143,42 @@ public class ContactDetailsDialog : Gtk.Dialog {
|
||||||
Dino.Application app = Application.get_default() as Dino.Application;
|
Dino.Application app = Application.get_default() as Dino.Application;
|
||||||
XmppStream? stream = app.stream_interactor.get_stream(account);
|
XmppStream? stream = app.stream_interactor.get_stream(account);
|
||||||
if (stream == null) return;
|
if (stream == null) return;
|
||||||
StreamModule? module = stream.get_module(StreamModule.IDENTITY);
|
Legacy.StreamModule? legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY);
|
||||||
if (module == null) return;
|
V1.StreamModule? v1_module = stream.get_module(V1.StreamModule.IDENTITY);
|
||||||
module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => {
|
if (legacy_module != null) {
|
||||||
if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) {
|
legacy_module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => {
|
||||||
Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id);
|
if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) {
|
||||||
if (device == null) return;
|
Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id);
|
||||||
if (auto_accept_switch.active) {
|
if (device == null) return;
|
||||||
add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]);
|
if (auto_accept_switch.active) {
|
||||||
} else {
|
add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]);
|
||||||
add_new_fingerprint(device);
|
} else {
|
||||||
|
add_new_fingerprint(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
if (v1_module != null) {
|
||||||
|
v1_module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => {
|
||||||
|
if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) {
|
||||||
|
Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id);
|
||||||
|
if (device == null) return;
|
||||||
|
if (auto_accept_switch.active) {
|
||||||
|
add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]);
|
||||||
|
} else {
|
||||||
|
add_new_fingerprint(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) {
|
foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) {
|
||||||
try {
|
try {
|
||||||
module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false);
|
if (legacy_module != null) {
|
||||||
|
legacy_module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false);
|
||||||
|
}
|
||||||
|
if (v1_module != null) {
|
||||||
|
v1_module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false);
|
||||||
|
}
|
||||||
} catch (InvalidJidError e) {
|
} catch (InvalidJidError e) {
|
||||||
warning("Ignoring device with invalid Jid: %s", e.message);
|
warning("Ignoring device with invalid Jid: %s", e.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class DeviceNotificationPopulator : NotificationPopulator, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_account_added(Account account) {
|
private void on_account_added(Account account) {
|
||||||
stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
|
stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
|
||||||
if (current_conversation != null && jid.equals(current_conversation.counterpart) && plugin.has_new_devices(current_conversation.account, current_conversation.counterpart)) {
|
if (current_conversation != null && jid.equals(current_conversation.counterpart) && plugin.has_new_devices(current_conversation.account, current_conversation.counterpart)) {
|
||||||
display_notification();
|
display_notification();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@ public class OwnNotifications {
|
||||||
this.stream_interactor = (!)stream_interactor;
|
this.stream_interactor = (!)stream_interactor;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
|
stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
|
||||||
|
if (jid.equals(account.bare_jid) && plugin.has_new_devices(account, account.bare_jid)) {
|
||||||
|
display_notification();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
|
||||||
if (jid.equals(account.bare_jid) && plugin.has_new_devices(account, account.bare_jid)) {
|
if (jid.equals(account.bare_jid) && plugin.has_new_devices(account, account.bare_jid)) {
|
||||||
display_notification();
|
display_notification();
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,12 @@ namespace Xmpp.Xep.Pubsub {
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate void OnResult(XmppStream stream, Jid jid, string? id, StanzaNode? node);
|
public delegate void OnResult(XmppStream stream, Jid jid, string? id, StanzaNode? node);
|
||||||
public void request(XmppStream stream, Jid jid, string node, owned OnResult listener) { // TODO multiple nodes gehen auch
|
public void request(XmppStream stream, Jid jid, string node, owned OnResult listener, string? item_id = null) { // TODO multiple nodes gehen auch
|
||||||
Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(new StanzaNode.build("items", NS_URI).put_attribute("node", node)));
|
var items = new StanzaNode.build("items", NS_URI).put_attribute("node", node);
|
||||||
|
if (item_id != null) {
|
||||||
|
items.put_node(new StanzaNode.build("item", NS_URI).put_attribute("id", item_id));
|
||||||
|
}
|
||||||
|
Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(items));
|
||||||
request_iq.to = jid;
|
request_iq.to = jid;
|
||||||
stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_iq, (stream, iq) => {
|
stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_iq, (stream, iq) => {
|
||||||
StanzaNode event_node = iq.stanza.get_subnode("pubsub", NS_URI);
|
StanzaNode event_node = iq.stanza.get_subnode("pubsub", NS_URI);
|
||||||
|
|
Loading…
Reference in a new issue