using Crypto; using Dino; using Dino.Entities; using Gee; using Signal; using Xmpp; using Xmpp.Xep; namespace Dino.Plugins.JetOmemo { private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0"; private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; public class Module : XmppStreamModule, Jet.EnvelopEncoding { public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0396_jet_omemo"); private Omemo.Plugin plugin; const uint KEY_SIZE = 16; const uint IV_SIZE = 12; public Module(Omemo.Plugin plugin) { this.plugin = plugin; } public override void attach(XmppStream stream) { if (stream.get_module(Jet.Module.IDENTITY) != null) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(Jet.Module.IDENTITY).register_envelop_encoding(this); stream.get_module(Jet.Module.IDENTITY).register_cipher(new AesGcmCipher(KEY_SIZE, IV_SIZE, AES_128_GCM_URI)); } } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); } public async bool is_available(XmppStream stream, Jid full_jid) { bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); if (has_feature == null || !(!)has_feature) { return false; } return yield stream.get_module(Xep.Jet.Module.IDENTITY).is_available(stream, full_jid); } public string get_type_uri() { return Omemo.NS_URI; } 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; StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI); 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); 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"); if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element"); uint8[] iv = Base64.decode((!)iv_node); foreach (StanzaNode key_node in header.get_subnodes("key")) { if (key_node.get_attribute_int("rid") == store.local_registration_id) { string? key_node_content = key_node.get_string_content(); uint8[] key; Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid")); if (key_node.get_attribute_bool("prekey")) { PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); SessionCipher cipher = store.create_session_cipher(address); key = cipher.decrypt_pre_key_signal_message(msg); } else { SignalMessage msg = Omemo.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 uint8[] authtag = null; if (key.length >= 32) { int authtaglength = key.length - 16; authtag = new uint8[authtaglength]; uint8[] new_key = new uint8[16]; Memory.copy(authtag, (uint8*)key + 16, 16); Memory.copy(new_key, key, 16); key = new_key; } // TODO: authtag? return new Jet.TransportSecret(key, iv); } } throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device"); } public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) { ArrayList accounts = plugin.app.stream_interactor.get_accounts(); Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; Account? account = null; foreach (Account compare in accounts) { if (compare.bare_jid.equals_bare(local_full_jid)) { account = compare; break; } } if (account == null) { // TODO critical("Sending from offline account %s", local_full_jid.to_string()); } StanzaNode header_node; StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns() .put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI) .put_attribute("sid", store.local_registration_id.to_string()) .put_node(new StanzaNode.build("iv", Omemo.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.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account); security.put_node(encrypted_node); } public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } } public class AesGcmCipher : Jet.Cipher, Object { private uint key_size; private uint default_iv_size; private string uri; public AesGcmCipher(uint key_size, uint default_iv_size, string uri) { this.key_size = key_size; this.default_iv_size = default_iv_size; this.uri = uri; } public string get_cipher_uri() { return uri; } public Jet.TransportSecret generate_random_secret() { uint8[] iv = new uint8[default_iv_size]; Omemo.Plugin.get_context().randomize(iv); uint8[] key = new uint8[key_size]; Omemo.Plugin.get_context().randomize(key); return new Jet.TransportSecret(key, iv); } public InputStream wrap_input_stream(InputStream input, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) { SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); cipher.set_key(secret.transport_key); cipher.set_iv(secret.initialization_vector); return new ConverterInputStream(input, new SymmetricCipherDecrypter((owned) cipher, 16)); } public OutputStream wrap_output_stream(OutputStream output, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) { Crypto.SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); cipher.set_key(secret.transport_key); cipher.set_iv(secret.initialization_vector); return new ConverterOutputStream(output, new SymmetricCipherEncrypter((owned) cipher, 16)); } } }