Add JET support

This commit is contained in:
Marvin W 2019-09-10 20:56:00 +02:00
parent 87d64524c8
commit e899668213
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
17 changed files with 438 additions and 139 deletions

View file

@ -71,45 +71,44 @@ public class FileManager : StreamInteractionModule, Object {
file_meta.size = file_transfer.size;
file_meta.mime_type = file_transfer.mime_type;
bool encrypted = false;
foreach (FileEncryptor file_encryptor in file_encryptors) {
if (file_encryptor.can_encrypt_file(conversation, file_transfer)) {
file_meta = file_encryptor.encrypt_file(conversation, file_transfer);
encrypted = true;
break;
}
}
if (conversation.encryption != Encryption.NONE && !encrypted) {
throw new FileSendError.ENCRYPTION_FAILED("File was not encrypted");
}
FileSendData file_send_data = null;
foreach (FileSender file_sender in file_senders) {
if (file_sender.can_send(conversation, file_transfer)) {
file_send_data = yield file_sender.prepare_send_file(conversation, file_transfer, file_meta);
break;
FileSender file_sender = null;
FileEncryptor file_encryptor = null;
foreach (FileSender sender in file_senders) {
if (sender.can_send(conversation, file_transfer)) {
if (file_transfer.encryption == Encryption.NONE || sender.can_encrypt(conversation, file_transfer)) {
file_sender = sender;
break;
} else {
foreach (FileEncryptor encryptor in file_encryptors) {
if (encryptor.can_encrypt_file(conversation, file_transfer)) {
file_encryptor = encryptor;
break;
}
}
if (file_encryptor != null) {
file_sender = sender;
break;
}
}
}
}
foreach (FileEncryptor file_encryptor in file_encryptors) {
if (file_encryptor.can_encrypt_file(conversation, file_transfer)) {
file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta);
break;
}
if (file_sender == null) {
throw new FileSendError.UPLOAD_FAILED("No sender/encryptor combination available");
}
bool sent = false;
foreach (FileSender file_sender in file_senders) {
if (file_sender.can_send(conversation, file_transfer)) {
yield file_sender.send_file(conversation, file_transfer, file_send_data);
sent = true;
break;
}
if (file_encryptor != null) {
file_meta = file_encryptor.encrypt_file(conversation, file_transfer);
}
if (!sent) {
throw new FileSendError.UPLOAD_FAILED("File was not sent");
FileSendData file_send_data = yield file_sender.prepare_send_file(conversation, file_transfer, file_meta);
if (file_encryptor != null) {
file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta);
}
yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta);
conversation.last_active = file_transfer.time;
} catch (Error e) {
warning("Send file error: %s", e.message);
@ -130,7 +129,9 @@ public class FileManager : StreamInteractionModule, Object {
yield download_file_internal(file_provider, file_transfer, conversation);
}
public bool is_upload_available(Conversation conversation) {
public bool is_upload_available(Conversation? conversation) {
if (conversation == null) return false;
foreach (FileSender file_sender in file_senders) {
if (file_sender.is_upload_available(conversation)) return true;
}
@ -230,12 +231,18 @@ public class FileManager : StreamInteractionModule, Object {
try {
// Get meta info
FileReceiveData receive_data = file_provider.get_file_receive_data(file_transfer);
foreach (FileDecryptor file_decryptor in file_decryptors) {
if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
receive_data = file_decryptor.prepare_get_meta_info(conversation, file_transfer, receive_data);
FileDecryptor? file_decryptor = null;
foreach (FileDecryptor decryptor in file_decryptors) {
if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
file_decryptor = decryptor;
break;
}
}
if (file_decryptor != null) {
receive_data = file_decryptor.prepare_get_meta_info(conversation, file_transfer, receive_data);
}
FileMeta file_meta = yield get_file_meta(file_provider, file_transfer, conversation, receive_data);
@ -244,34 +251,21 @@ public class FileManager : StreamInteractionModule, Object {
// Download and decrypt file
file_transfer.state = FileTransfer.State.IN_PROGRESS;
foreach (FileDecryptor file_decryptor in file_decryptors) {
if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta);
break;
}
if (file_decryptor != null) {
file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta);
}
input_stream = yield file_provider.download(file_transfer, receive_data, file_meta);
foreach (FileDecryptor file_decryptor in file_decryptors) {
if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data);
break;
}
if (file_decryptor != null) {
input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data);
}
// Save file
string filename = Random.next_int().to_string("%x") + "_" + file_meta.file_name;
string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name;
File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename));
if (file_transfer.encryption == Encryption.PGP || file.get_path().has_suffix(".pgp")) {
file = File.new_for_path(file.get_path().substring(0, file.get_path().length - 4));
}
OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION);
yield os.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET);
file_transfer.size = (int)file_meta.size;
file_transfer.file_name = file_meta.file_name;
file_transfer.path = file.get_basename();
file_transfer.input_stream = yield file.read_async();
@ -392,7 +386,8 @@ public interface FileSender : Object {
public abstract bool is_upload_available(Conversation conversation);
public abstract bool can_send(Conversation conversation, FileTransfer file_transfer);
public abstract async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError;
public abstract async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError;
public abstract async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError;
public abstract bool can_encrypt(Conversation conversation, FileTransfer file_transfer);
public abstract int get_id();
public abstract float get_priority();

View file

@ -6,6 +6,56 @@ using Dino.Entities;
namespace Dino {
public interface JingleFileEncryptionHelper : Object {
public abstract bool can_transfer(Conversation conversation);
public abstract bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid = null);
public abstract string? get_precondition_name(Conversation conversation, FileTransfer file_transfer);
public abstract Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer);
public abstract FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer);
}
public class JingleFileEncryptionHelperTransferOnly : JingleFileEncryptionHelper, Object {
public bool can_transfer(Conversation conversation) {
return true;
}
public bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid) {
return false;
}
public string? get_precondition_name(Conversation conversation, FileTransfer file_transfer) {
return null;
}
public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) {
return null;
}
public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) {
return file_meta;
}
}
public class JingleFileHelperRegistry {
private static JingleFileHelperRegistry INSTANCE;
public static JingleFileHelperRegistry instance { get {
if (INSTANCE == null) {
INSTANCE = new JingleFileHelperRegistry();
INSTANCE.add_encryption_helper(Encryption.NONE, new JingleFileEncryptionHelperTransferOnly());
}
return INSTANCE;
} }
internal HashMap<Encryption, JingleFileEncryptionHelper> encryption_helpers = new HashMap<Encryption, JingleFileEncryptionHelper>();
public void add_encryption_helper(Encryption encryption, JingleFileEncryptionHelper helper) {
encryption_helpers[encryption] = helper;
}
public JingleFileEncryptionHelper? get_encryption_helper(Encryption encryption) {
if (encryption_helpers.has_key(encryption)) {
return encryption_helpers[encryption];
}
return null;
}
}
public class JingleFileProvider : FileProvider, Object {
private StreamInteractor stream_interactor;
@ -29,7 +79,15 @@ public class JingleFileProvider : FileProvider, Object {
}
public async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError {
return file_meta;
Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info];
if (jingle_file_transfer == null) {
throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
}
FileMeta meta = file_meta;
foreach (JingleFileEncryptionHelper helper in JingleFileHelperRegistry.instance.encryption_helpers.values) {
meta = helper.complete_meta(file_transfer, receive_data, meta, jingle_file_transfer);
}
return meta;
}
public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError {
@ -39,6 +97,9 @@ public class JingleFileProvider : FileProvider, Object {
if (jingle_file_transfer == null) {
throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
}
foreach (JingleFileEncryptionHelper helper in JingleFileHelperRegistry.instance.encryption_helpers.values) {
helper.complete_meta(file_transfer, receive_data, file_meta, jingle_file_transfer);
}
try {
jingle_file_transfer.accept(stream);
} catch (IOError e) {
@ -83,6 +144,10 @@ public class JingleFileSender : FileSender, Object {
}
public bool is_upload_available(Conversation conversation) {
JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(conversation.encryption);
if (helper == null) return false;
if (!helper.can_transfer(conversation)) return false;
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream == null) return false;
@ -98,32 +163,46 @@ public class JingleFileSender : FileSender, Object {
}
public bool can_send(Conversation conversation, FileTransfer file_transfer) {
if (conversation.encryption != Encryption.NONE) return false;
// No file specific restrictions apply to Jingle file transfers
return is_upload_available(conversation);
}
XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
if (stream == null) return false;
foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) {
if (stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
return true;
}
}
return false;
public bool can_encrypt(Conversation conversation, FileTransfer file_transfer) {
JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
if (helper == null) return false;
return helper.can_encrypt(conversation, file_transfer);
}
public async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError {
if (file_meta is HttpFileMeta) {
throw new FileSendError.UPLOAD_FAILED("Cannot upload http file meta over Jingle");
}
return new FileSendData();
}
public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError {
// TODO(hrxi) What should happen if `stream == null`?
public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError {
XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream available");
JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
bool must_encrypt = helper != null && helper.can_encrypt(conversation, file_transfer);
foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) {
// TODO(hrxi): Prioritization of transports (and resources?).
if (!stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
continue;
}
stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).offer_file_stream.begin(stream, full_jid, file_transfer.input_stream, file_transfer.file_name, file_transfer.size);
if (must_encrypt && !helper.can_encrypt(conversation, file_transfer, full_jid)) {
continue;
}
string? precondition_name = null;
Object? precondition_options = null;
if (must_encrypt) {
precondition_name = helper.get_precondition_name(conversation, file_transfer);
precondition_options = helper.get_precondition_options(conversation, file_transfer);
if (precondition_name == null) {
throw new FileSendError.ENCRYPTION_FAILED("Should have created a precondition, but did not");
}
}
yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).offer_file_stream(stream, full_jid, file_transfer.input_stream, file_transfer.server_file_name, file_meta.size, precondition_name, precondition_options);
return;
}
}

View file

@ -84,6 +84,7 @@ public class ModuleManager {
module_map[account].add(new Xep.JingleSocks5Bytestreams.Module());
module_map[account].add(new Xep.JingleInBandBytestreams.Module());
module_map[account].add(new Xep.JingleFileTransfer.Module());
module_map[account].add(new Xep.Jet.Module());
initialize_account_modules(account, module_map[account]);
}
}

View file

@ -57,6 +57,7 @@ public class View : Box {
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
encryption_widget.get_style_context().add_class("dino-chatinput-button");
encryption_widget.encryption_changed.connect(update_file_transfer_availability);
// Emoji button for emoji picker (recents don't work < 3.22.19, category icons don't work <3.23.2)
if (Gtk.get_major_version() >= 3 && Gtk.get_minor_version() >= 24) {
@ -83,15 +84,17 @@ public class View : Box {
return this;
}
public void initialize_for_conversation(Conversation conversation) {
if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text;
this.conversation = conversation;
private void update_file_transfer_availability() {
bool upload_available = stream_interactor.get_module(FileManager.IDENTITY).is_upload_available(conversation);
file_button.visible = upload_available;
file_separator.visible = upload_available;
}
public void initialize_for_conversation(Conversation conversation) {
if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text;
this.conversation = conversation;
update_file_transfer_availability();
text_input.buffer.text = "";
if (entry_cache.has_key(conversation)) {

View file

@ -163,11 +163,11 @@ private static uint8[] get_uint8_from_data(Data data) {
data.seek(0);
uint8[] buf = new uint8[256];
ssize_t? len = null;
Array<uint8> res = new Array<uint8>(false, true, 0);
ByteArray res = new ByteArray();
do {
len = data.read(buf);
if (len > 0) {
res.append_vals(buf, (int)len);
res.append(buf[0:len]);
}
} while (len > 0);
return res.data;

View file

@ -35,11 +35,11 @@ public class HttpFileSender : FileSender, Object {
return send_data;
}
public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError {
public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError {
HttpFileSendData? send_data = file_send_data as HttpFileSendData;
if (send_data == null) return;
yield upload(file_transfer, send_data);
yield upload(file_transfer, send_data, file_meta);
file_transfer.info = send_data.url_down; // store the message content temporarily so the message gets filtered out
@ -62,6 +62,10 @@ public class HttpFileSender : FileSender, Object {
return file_transfer.size < max_file_sizes[conversation.account];
}
public bool can_encrypt(Conversation conversation, FileTransfer file_transfer) {
return false;
}
public bool is_upload_available(Conversation conversation) {
lock (max_file_sizes) {
return max_file_sizes.has_key(conversation.account);
@ -74,24 +78,27 @@ public class HttpFileSender : FileSender, Object {
}
}
private async void upload(FileTransfer file_transfer, HttpFileSendData file_send_data) throws FileSendError {
private static void transfer_more_bytes(InputStream stream, Soup.MessageBody body) {
uint8[] bytes = new uint8[4096];
ssize_t read = stream.read(bytes);
if (read == 0) {
body.complete();
return;
}
bytes.length = (int)read;
body.append_buffer(new Soup.Buffer.take(bytes));
}
private async void upload(FileTransfer file_transfer, HttpFileSendData file_send_data, FileMeta file_meta) throws FileSendError {
Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
if (stream == null) return;
uint8[] buf = new uint8[256];
Array<uint8> data = new Array<uint8>(false, true, 0);
size_t len = -1;
do {
try {
len = file_transfer.input_stream.read(buf);
} catch (IOError e) {
throw new FileSendError.UPLOAD_FAILED("HTTP upload: IOError reading stream: %s".printf(e.message));
}
data.append_vals(buf, (uint) len);
} while(len > 0);
Soup.Message message = new Soup.Message("PUT", file_send_data.url_up);
message.set_request(file_transfer.mime_type, Soup.MemoryUse.COPY, data.data);
message.request_headers.set_content_type(file_meta.mime_type, null);
message.request_headers.set_content_length(file_meta.size);
message.request_body.set_accumulate(false);
message.wrote_headers.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body));
message.wrote_chunk.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body));
Soup.Session session = new Soup.Session();
try {
yield session.send_async(message);

View file

@ -1,5 +1,6 @@
using Dino.Entities;
using Crypto;
using Signal;
namespace Dino.Plugins.Omemo {
@ -56,20 +57,17 @@ public class OmemoFileDecryptor : FileDecryptor, Object {
key = iv_and_key[16:48];
}
// Read data
uint8[] buf = new uint8[256];
Array<uint8> data = new Array<uint8>(false, true, 0);
size_t len = -1;
do {
len = yield encrypted_stream.read_async(buf);
data.append_vals(buf, (uint) len);
} while(len > 0);
// Decrypt
uint8[] cleartext = Signal.aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data);
file_transfer.encryption = Encryption.OMEMO;
return new MemoryInputStream.from_data(cleartext);
} catch (Error e) {
debug("Decrypting file %s from %s", file_transfer.file_name, file_transfer.server_file_name);
SymmetricCipher cipher = new SymmetricCipher("AES-GCM");
cipher.set_key(key);
cipher.set_iv(iv);
return new ConverterInputStream(encrypted_stream, new SymmetricCipherDecrypter((owned) cipher));
} catch (Crypto.Error e) {
throw new FileReceiveError.DECRYPTION_FAILED("OMEMO file decryption error: %s".printf(e.message));
} catch (GLib.Error e) {
throw new FileReceiveError.DECRYPTION_FAILED("OMEMO file decryption error: %s".printf(e.message));
}
}

View file

@ -1,6 +1,7 @@
using Gee;
using Gtk;
using Crypto;
using Dino.Entities;
using Xmpp;
using Signal;
@ -22,30 +23,29 @@ public class OmemoFileEncryptor : Dino.FileEncryptor, Object {
var omemo_http_file_meta = new OmemoHttpFileMeta();
try {
uint8[] buf = new uint8[256];
Array<uint8> data = new Array<uint8>(false, true, 0);
size_t len = -1;
do {
len = file_transfer.input_stream.read(buf);
data.append_vals(buf, (uint) len);
} while(len > 0);
//Create a key and use it to encrypt the file
uint8[] iv = new uint8[16];
Plugin.get_context().randomize(iv);
uint8[] key = new uint8[32];
Plugin.get_context().randomize(key);
uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data);
SymmetricCipher cipher = new SymmetricCipher("AES-GCM");
cipher.set_key(key);
cipher.set_iv(iv);
omemo_http_file_meta.iv = iv;
omemo_http_file_meta.key = key;
omemo_http_file_meta.size = ciphertext.length;
omemo_http_file_meta.mime_type = "pgp";
file_transfer.input_stream = new MemoryInputStream.from_data(ciphertext, GLib.free);
} catch (Error error) {
throw new FileSendError.ENCRYPTION_FAILED("HTTP upload: Error encrypting stream: %s".printf(error.message));
omemo_http_file_meta.size = file_transfer.size;
omemo_http_file_meta.mime_type = "omemo";
file_transfer.input_stream = new ConverterInputStream(file_transfer.input_stream, new SymmetricCipherEncrypter((owned) cipher));
} catch (Crypto.Error error) {
throw new FileSendError.ENCRYPTION_FAILED("OMEMO file encryption error: %s".printf(error.message));
} catch (GLib.Error error) {
throw new FileSendError.ENCRYPTION_FAILED("OMEMO file encryption error: %s".printf(error.message));
}
debug("Encrypting file %s as %s", file_transfer.file_name, file_transfer.server_file_name);
return omemo_http_file_meta;
}

View file

@ -53,7 +53,7 @@ GRESOURCES
${OPENPGP_GRESOURCES_XML}
)
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\")
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="OpenPGP" -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\")
add_library(openpgp SHARED ${OPENPGP_VALA_C} ${OPENPGP_GRESOURCES_TARGET})
add_dependencies(openpgp ${GETTEXT_PACKAGE}-translations)
target_link_libraries(openpgp libdino gpgme-vala ${OPENPGP_PACKAGES})

View file

@ -19,18 +19,20 @@ public class PgpFileDecryptor : FileDecryptor, Object {
public async InputStream decrypt_file(InputStream encrypted_stream, Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) throws FileReceiveError {
try {
uint8[] buf = new uint8[256];
Array<uint8> data = new Array<uint8>(false, true, 0);
ByteArray data = new ByteArray();
size_t len = -1;
do {
len = encrypted_stream.read(buf);
data.append_vals(buf, (uint) len);
len = yield encrypted_stream.read_async(buf);
data.append(buf[0:len]);
} while(len > 0);
GPGHelper.DecryptedData clear_data = GPGHelper.decrypt_data(data.data);
file_transfer.encryption = Encryption.PGP;
if (clear_data.filename != null && clear_data.filename != "") {
debug("Decrypting file %s from %s", clear_data.filename, file_transfer.file_name);
file_transfer.file_name = clear_data.filename;
} else if (file_transfer.file_name.has_suffix(".pgp")) {
debug("Decrypting file %s from %s", file_transfer.file_name.substring(0, file_transfer.file_name.length - 4), file_transfer.file_name);
file_transfer.file_name = file_transfer.file_name.substring(0, file_transfer.file_name.length - 4);
}
return new MemoryInputStream.from_data(clear_data.data, GLib.free);

View file

@ -15,17 +15,21 @@ public class PgpFileEncryptor : Dino.FileEncryptor, Object {
}
public FileMeta encrypt_file(Conversation conversation, FileTransfer file_transfer) throws FileSendError {
FileMeta file_meta = new FileMeta();
try {
GPG.Key[] keys = stream_interactor.get_module(Manager.IDENTITY).get_key_fprs(conversation);
uint8[] enc_content = GPGHelper.encrypt_file(file_transfer.get_file().get_path(), keys, GPG.EncryptFlags.ALWAYS_TRUST, file_transfer.file_name);
file_transfer.input_stream = new MemoryInputStream.from_data(enc_content, GLib.free);
file_transfer.encryption = Encryption.PGP;
file_transfer.server_file_name = Xmpp.random_uuid() + ".pgp";
file_meta.size = enc_content.length;
} catch (Error e) {
throw new FileSendError.ENCRYPTION_FAILED("PGP file encryption error: %s".printf(e.message));
}
debug("Encrypting file %s as %s", file_transfer.file_name, file_transfer.server_file_name);
return new FileMeta();
return file_meta;
}
public FileSendData? preprocess_send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) {

View file

@ -31,6 +31,7 @@ public class Plugin : Plugins.RootInterface, Object {
Manager.start(app.stream_interactor, db);
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new PgpFileEncryptor(app.stream_interactor));
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new PgpFileDecryptor());
JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.PGP, new JingleFileEncryptionHelperTransferOnly());
internationalize(GETTEXT_PACKAGE, app.search_path_generator.get_locale_path(GETTEXT_PACKAGE, LOCALE_INSTALL_DIR));
}

View file

@ -78,6 +78,7 @@ SOURCES
"src/module/xep/0363_http_file_upload.vala"
"src/module/xep/0368_srv_records_tls.vala"
"src/module/xep/0380_explicit_encryption.vala"
"src/module/xep/0391_jingle_encrypted_transports.vala"
"src/module/xep/pixbuf_storage.vala"
"src/util.vala"
@ -95,7 +96,7 @@ DEPENDS
${CMAKE_BINARY_DIR}/exports/xmpp-vala.deps
)
add_definitions(${VALA_CFLAGS})
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="xmpp-vala")
add_library(xmpp-vala SHARED ${ENGINE_VALA_C})
add_dependencies(xmpp-vala xmpp-vala-vapi)
target_link_libraries(xmpp-vala ${ENGINE_PACKAGES})

View file

@ -43,6 +43,7 @@ public errordomain Error {
BAD_REQUEST,
INVALID_PARAMETERS,
UNSUPPORTED_TRANSPORT,
UNSUPPORTED_SECURITY,
NO_SHARED_PROTOCOLS,
TRANSPORT_ERROR,
}
@ -69,6 +70,7 @@ class ContentNode {
public string name;
public StanzaNode? description;
public StanzaNode? transport;
public StanzaNode? security;
}
ContentNode get_single_content_node(StanzaNode jingle) throws IqError {
@ -94,6 +96,7 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError {
string? name = content.get_attribute("name");
StanzaNode? description = get_single_node_anyns(content, "description");
StanzaNode? transport = get_single_node_anyns(content, "transport");
StanzaNode? security = get_single_node_anyns(content, "security");
if (name == null || creator == null) {
throw new IqError.BAD_REQUEST("missing name or creator");
}
@ -102,7 +105,8 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError {
creator=creator,
name=name,
description=description,
transport=transport
transport=transport,
security=security
};
}
@ -112,6 +116,7 @@ public class Module : XmppStreamModule, Iq.Handler {
private HashMap<string, ContentType> content_types = new HashMap<string, ContentType>();
private HashMap<string, Transport> transports = new HashMap<string, Transport>();
private HashMap<string, SecurityPrecondition> security_preconditions = new HashMap<string, SecurityPrecondition>();
private XmppStream? current_stream = null;
@ -163,6 +168,16 @@ public class Module : XmppStreamModule, Iq.Handler {
}
return result;
}
public void register_security_precondition(SecurityPrecondition precondition) {
security_preconditions[precondition.security_ns_uri()] = precondition;
}
public SecurityPrecondition? get_security_precondition(string? ns_uri) {
if (ns_uri == null) return null;
if (!security_preconditions.has_key(ns_uri)) {
return null;
}
return security_preconditions[ns_uri];
}
private bool is_jingle_available(XmppStream stream, Jid full_jid) {
bool? has_jingle = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI);
@ -173,7 +188,7 @@ public class Module : XmppStreamModule, Iq.Handler {
return is_jingle_available(stream, full_jid) && select_transport(stream, type, full_jid, Set.empty()) != null;
}
public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description) throws Error {
public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description, string? precondition_name = null, Object? precondation_options = null) throws Error {
if (!is_jingle_available(stream, receiver_full_jid)) {
throw new Error.NO_SHARED_PROTOCOLS("No Jingle support");
}
@ -181,18 +196,26 @@ public class Module : XmppStreamModule, Iq.Handler {
if (transport == null) {
throw new Error.NO_SHARED_PROTOCOLS("No suitable transports");
}
SecurityPrecondition? precondition = get_security_precondition(precondition_name);
if (precondition_name != null && precondition == null) {
throw new Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found");
}
Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
if (my_jid == null) {
throw new Error.GENERAL("Couldn't determine own JID");
}
TransportParameters transport_params = transport.create_transport_parameters(stream, my_jid, receiver_full_jid);
Session session = new Session.initiate_sent(random_uuid(), type, transport_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session);
SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondation_options) : null;
Session session = new Session.initiate_sent(random_uuid(), type, transport_params, security_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session);
StanzaNode content = new StanzaNode.build("content", NS_URI)
.put_attribute("creator", "initiator")
.put_attribute("name", content_name)
.put_attribute("senders", senders.to_string())
.put_node(description)
.put_node(transport_params.to_transport_stanza_node());
if (security_params != null) {
content.put_node(security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid));
}
StanzaNode jingle = new StanzaNode.build("jingle", NS_URI)
.add_self_xmlns()
.put_attribute("action", "session-initiate")
@ -233,8 +256,17 @@ public class Module : XmppStreamModule, Iq.Handler {
}
ContentParameters content_params = content_type.parse_content_parameters(content.description);
SecurityPrecondition? precondition = content.security != null ? get_security_precondition(content.security.ns_uri) : null;
SecurityParameters? security_params = null;
if (precondition != null) {
debug("Using precondition %s", precondition.security_ns_uri());
security_params = precondition.parse_security_parameters(stream, my_jid, iq.from, content.security);
} else if (content.security != null) {
throw new IqError.NOT_IMPLEMENTED("unknown security precondition");
}
TransportType type = content_type.content_type_transport_type();
Session session = new Session.initiate_received(sid, type, transport_params, my_jid, iq.from, content.name, send_terminate_and_remove_session);
Session session = new Session.initiate_received(sid, type, transport_params, security_params, my_jid, iq.from, content.name, send_terminate_and_remove_session);
stream.get_flag(Flag.IDENTITY).add_session(session);
stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq));
@ -328,7 +360,7 @@ public interface Transport : Object {
public abstract bool is_transport_available(XmppStream stream, Jid full_jid);
public abstract TransportType transport_type();
public abstract int transport_priority();
public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid);
public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) throws Error;
public abstract TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError;
}
@ -375,6 +407,17 @@ public interface ContentParameters : Object {
public abstract void on_session_initiate(XmppStream stream, Session session);
}
public interface SecurityPrecondition : Object {
public abstract string security_ns_uri();
public abstract SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error;
public abstract SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError;
}
public interface SecurityParameters : Object {
public abstract string security_ns_uri();
public abstract StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid);
public abstract IOStream wrap_stream(IOStream stream);
}
public class Session {
// INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED
@ -398,6 +441,7 @@ public class Session {
public Jid peer_full_jid { get; private set; }
public Role content_creator { get; private set; }
public string content_name { get; private set; }
public SecurityParameters? security { get; private set; }
private Connection connection;
public IOStream conn { get { return connection; } }
@ -410,7 +454,7 @@ public class Session {
SessionTerminate session_terminate_handler;
public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) {
public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) {
this.state = State.INITIATE_SENT;
this.role = Role.INITIATOR;
this.sid = sid;
@ -422,12 +466,13 @@ public class Session {
this.tried_transport_methods = new HashSet<string>();
this.tried_transport_methods.add(transport.transport_ns_uri());
this.transport = transport;
this.security = security;
this.connection = new Connection(this);
this.session_terminate_handler = (owned)session_terminate_handler;
this.terminate_on_connection_close = true;
}
public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) {
public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) {
this.state = State.INITIATE_RECEIVED;
this.role = Role.RESPONDER;
this.sid = sid;
@ -437,6 +482,7 @@ public class Session {
this.content_creator = Role.INITIATOR;
this.content_name = content_name;
this.transport = transport;
this.security = security;
this.tried_transport_methods = new HashSet<string>();
if (transport != null) {
this.tried_transport_methods.add(transport.transport_ns_uri());
@ -557,7 +603,12 @@ public class Session {
state = State.ACTIVE;
transport = null;
tried_transport_methods.clear();
connection.set_inner(conn);
if (security != null) {
connection.set_inner(security.wrap_stream(conn));
} else {
connection.set_inner(conn);
}
} else {
if (role == Role.INITIATOR) {
select_new_transport(stream);
@ -913,6 +964,7 @@ public class Connection : IOStream {
return true;
}
public async bool close_read_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
debug("Closing Jingle input stream");
yield wait_and_check_for_errors(io_priority, cancellable);
if (read_closed) {
return true;

View file

@ -48,18 +48,24 @@ public class Module : Jingle.ContentType, XmppStreamModule {
return stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid);
}
public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size) throws IOError {
public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError {
StanzaNode file_node;
StanzaNode description = new StanzaNode.build("description", NS_URI)
.add_self_xmlns()
.put_node(new StanzaNode.build("file", NS_URI)
.put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename)))
.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string()))));
.put_node(file_node = new StanzaNode.build("file", NS_URI)
.put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename))));
// TODO(hrxi): Add the mandatory hash field
if (size > 0) {
file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string())));
} else {
warning("Sending file %s without size, likely going to cause problems down the road...", basename);
}
Jingle.Session session;
try {
session = stream.get_module(Jingle.Module.IDENTITY)
.create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description); // TODO(hrxi): Why "a-file-offer"?
.create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"?
} catch (Jingle.Error e) {
throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)");
}
@ -172,13 +178,14 @@ public class FileTransfer : Object {
public Jid peer { get { return session.peer_full_jid; } }
public string? file_name { get { return parameters.name; } }
public int64 size { get { return parameters.size; } }
public Jingle.SecurityParameters? security { get { return session.security; } }
public InputStream? stream { get; private set; }
public FileTransfer(Jingle.Session session, Parameters parameters) {
this.session = session;
this.parameters = parameters;
this.stream = new FileTransferInputStream(session.conn.input_stream, parameters.size);
this.stream = new FileTransferInputStream(session.conn.input_stream, size);
}
public void accept(XmppStream stream) throws IOError {

View file

@ -287,6 +287,7 @@ class Parameters : Jingle.TransportParameters, Object {
}
remote_sent_selected_candidate = true;
remote_selected_candidate = candidate;
debug("Remote selected candidate %s", candidate.cid);
try_completing_negotiation();
}
private void handle_activated(string cid) throws Jingle.IqError {
@ -353,6 +354,7 @@ class Parameters : Jingle.TransportParameters, Object {
}
}
public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) {
debug("Waiting for remote activation of %s", candidate.cid);
waiting_for_activation_cid = candidate.cid;
waiting_for_activation_callback = wait_for_remote_activation.callback;
yield;
@ -368,6 +370,7 @@ class Parameters : Jingle.TransportParameters, Object {
}
}
public async void connect_to_local_candidate(Candidate candidate) {
debug("Connecting to candidate %s", candidate.cid);
try {
SocketConnection conn = yield connect_to_socks5(candidate, local_dstaddr);
@ -420,6 +423,7 @@ class Parameters : Jingle.TransportParameters, Object {
SocketClient socket_client = new SocketClient() { timeout=3 };
string address = @"[$(candidate.host)]:$(candidate.port)";
debug("Connecting to SOCKS5 server at %s", address);
size_t written;
size_t read;
@ -500,6 +504,7 @@ class Parameters : Jingle.TransportParameters, Object {
local_determined_selected_candidate = true;
local_selected_candidate = candidate;
local_selected_candidate_conn = conn;
debug("Selected candidate %s", candidate.cid);
session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI)
.add_self_xmlns()
.put_attribute("sid", sid)
@ -522,6 +527,8 @@ class Parameters : Jingle.TransportParameters, Object {
.put_attribute("sid", sid)
.put_node(new StanzaNode.build("candidate-error", NS_URI))
);
// Try remote candidates
try_completing_negotiation();
}
public void create_transport_connection(XmppStream stream, Jingle.Session session) {
this.session = session;

View file

@ -0,0 +1,142 @@
using Gee;
using Xmpp.Xep.Jingle;
namespace Xmpp.Xep.Jet {
public const string NS_URI = "urn:xmpp:jingle:jet:0";
public class Module : XmppStreamModule, SecurityPrecondition {
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0391_jet");
private HashMap<string, EnvelopEncoding> envelop_encodings = new HashMap<string, EnvelopEncoding>();
private HashMap<string, Cipher> ciphers = new HashMap<string, Cipher>();
public override void attach(XmppStream stream) {
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
stream.get_module(Jingle.Module.IDENTITY).register_security_precondition(this);
}
public override void detach(XmppStream stream) {
}
public bool is_available(XmppStream stream, Jid full_jid) {
bool? has_feature = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI);
return has_feature != null && (!)has_feature;
}
public void register_envelop_encoding(EnvelopEncoding encoding) {
envelop_encodings[encoding.get_type_uri()] = encoding;
}
public void register_cipher(Cipher cipher) {
ciphers[cipher.get_cipher_uri()] = cipher;
}
public string security_ns_uri() {
return NS_URI;
}
public Jingle.SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error requires (options is Options) {
Options jet_options = (Options) options;
string cipher = jet_options.cipher_uri;
string type = jet_options.type_uri;
if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) {
throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown");
}
EnvelopEncoding encoding = envelop_encodings[type];
return new SecurityParameters(ciphers[cipher], encoding, ciphers[cipher].generate_random_secret(), jet_options);
}
public Jingle.SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError {
string? cipher = security.get_attribute("cipher");
string? type = security.get_attribute("type");
if (cipher == null || type == null) {
throw new IqError.BAD_REQUEST("No cipher or type specified for JET");
}
if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) {
throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown");
}
EnvelopEncoding encoding = envelop_encodings[type];
TransportSecret secret = encoding.decode_envolop(stream, local_full_jid, peer_full_jid, security);
return new SecurityParameters(ciphers[cipher], encoding, secret);
}
public override string get_ns() { return NS_URI; }
public override string get_id() { return IDENTITY.id; }
}
public class Options : Object {
public string type_uri { get; private set; }
public string cipher_uri { get; private set; }
public Options(string type_uri, string cipher_uri) {
this.type_uri = type_uri;
this.cipher_uri = cipher_uri;
}
}
public class SecurityParameters : Jingle.SecurityParameters, Object {
public Cipher cipher { get; private set; }
public EnvelopEncoding encoding { get; private set; }
public TransportSecret secret { get; private set; }
public Options? options { get; private set; }
public SecurityParameters(Cipher cipher, EnvelopEncoding encoding, TransportSecret secret, Options? options = null) {
this.cipher = cipher;
this.encoding = encoding;
this.secret = secret;
this.options = options;
}
public string security_ns_uri() {
return NS_URI;
}
public IOStream wrap_stream(IOStream stream) {
debug("Wrapping stream into encrypted stream for %s/%s", encoding.get_type_uri(), cipher.get_cipher_uri());
return new EncryptedStream(cipher, secret, stream);
}
public StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) {
StanzaNode security = new StanzaNode.build("security", NS_URI)
.add_self_xmlns()
.put_attribute("cipher", cipher.get_cipher_uri())
.put_attribute("type", encoding.get_type_uri());
encoding.encode_envelop(stream, local_full_jid, peer_full_jid, this, security);
return security;
}
}
public interface Cipher : Object {
public abstract string get_cipher_uri();
public abstract TransportSecret generate_random_secret();
public abstract InputStream wrap_input_stream(InputStream input, TransportSecret secret);
public abstract OutputStream wrap_output_stream(OutputStream output, TransportSecret secret);
}
private class EncryptedStream : IOStream {
private IOStream stream;
private InputStream input;
private OutputStream output;
public override InputStream input_stream { get { return input; } }
public override OutputStream output_stream { get { return output; } }
public EncryptedStream(Cipher cipher, TransportSecret secret, IOStream stream) {
this.stream = stream;
input = cipher.wrap_input_stream(stream.input_stream, secret);
output = cipher.wrap_output_stream(stream.output_stream, secret);
}
}
public class TransportSecret {
public uint8[] transport_key { get; private set; }
public uint8[] initialization_vector { get; private set; }
public TransportSecret(uint8[] transport_key, uint8[] initialization_vector) {
this.transport_key = transport_key;
this.initialization_vector = initialization_vector;
}
}
public interface EnvelopEncoding : Object {
public abstract string get_type_uri();
public abstract TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError;
public abstract void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, SecurityParameters security_params, StanzaNode security);
}
}