diff --git a/Makefile.am b/Makefile.am index 4cced10..c201b20 100644 --- a/Makefile.am +++ b/Makefile.am @@ -42,6 +42,7 @@ VALAFLAGS = \ -g \ --vapidir=vapi \ --pkg=gio-2.0 \ + --pkg=gio-unix-2.0 \ --pkg=json-glib-1.0 \ --pkg=gee-0.8 \ --pkg=libnotify \ diff --git a/src/mconnect/core.vala b/src/mconnect/core.vala index 5436307..5c12832 100644 --- a/src/mconnect/core.vala +++ b/src/mconnect/core.vala @@ -94,4 +94,19 @@ class Core : Object { return config; } + + public static TlsCertificate get_certificate() throws Error { + var cert_path = Path.build_filename(get_storage_dir(), + "certificate.pem"); + var key_path = Path.build_filename(get_storage_dir(), + "private.pem"); + TlsCertificate cert; + try { + cert = new TlsCertificate.from_files(cert_path, key_path); + } catch (Error e) { + warning("failed to load certificate or key: %s", e.message); + throw e; + } + return cert; + } } \ No newline at end of file diff --git a/src/mconnect/device.vala b/src/mconnect/device.vala index b0d02f0..08d5acf 100644 --- a/src/mconnect/device.vala +++ b/src/mconnect/device.vala @@ -49,7 +49,7 @@ class Device : Object { public string device_id { get; private set; default = ""; } public string device_name { get; private set; default = ""; } public string device_type { get; private set; default = ""; } - public uint protocol_version {get; private set; default = 5; } + public uint protocol_version {get; private set; default = 7; } public uint tcp_port {get; private set; default = 1714; } public InetAddress host { get; private set; default = null; } public bool is_paired { get; private set; default = false; } @@ -68,7 +68,7 @@ class Device : Object { } private HashSet _capabilities = null; - public string public_key {get; private set; default = ""; } + public string certificate { get; private set; default = ""; } // set to true if pair request was sent private bool _pair_in_progress = false; @@ -130,7 +130,7 @@ class Device : Object { debug("last known address: %s:%u", last_ip_str, dev.tcp_port); dev.allowed = cache.get_boolean(name, "allowed"); dev.is_paired = cache.get_boolean(name, "paired"); - dev.public_key = cache.get_string(name, "public_key"); + dev.certificate = cache.get_string(name, "certificate"); dev.outgoing_capabilities = new ArrayList.wrap( cache.get_string_list(name, "outgoing_capabilities")); @@ -188,7 +188,7 @@ class Device : Object { cache.set_string(name, "lastIPAddress", this.host.to_string()); cache.set_boolean(name, "allowed", this.allowed); cache.set_boolean(name, "paired", this.is_paired); - cache.set_string(name, "public_key", this.public_key); + cache.set_string(name, "certificate", this.certificate); cache.set_string_list(name, "outgoing_capabilities", array_list_to_list(this.outgoing_capabilities)); cache.set_string_list(name, "incoming_capabilities", @@ -203,7 +203,29 @@ class Device : Object { Environment.get_host_name(), core.handlers.interfaces, core.handlers.interfaces)); - this.maybe_pair(); + + TlsCertificate? expected_cert = null; + if (this.certificate != "") { + try { + expected_cert = new TlsCertificate.from_pem(this.certificate, + this.certificate.length); + } catch (Error e) { + warning("failed to parse cached PEM cert of device %s: %s", + this.device_id, e.message); + } + } + // switch to secure channel + var secure = yield _channel.secure(expected_cert); + info("secure: %s", secure.to_string()); + + if (secure) { + this.certificate = _channel.peer_certificate.certificate_pem; + + this.maybe_pair(); + } else { + warning("failed to enable secure channel"); + close_and_cleanup(); + } } /** @@ -217,10 +239,6 @@ class Device : Object { if (this.host != null) { debug("start pairing"); - var core = Core.instance(); - string pubkey = core.crypt.get_public_key_pem(); - debug("public key: %s", pubkey); - if (expect_response == true) { _pair_in_progress = true; // pairing timeout @@ -228,7 +246,7 @@ class Device : Object { this.pair_timeout); } // send request - yield _channel.send(Packet.new_pair(pubkey)); + yield _channel.send(Packet.new_pair()); } } @@ -238,7 +256,7 @@ class Device : Object { _pair_timeout_source = 0; // handle failed pairing - handle_pair(false, ""); + handle_pair(false); // remove timeout source return false; @@ -255,7 +273,7 @@ class Device : Object { this.pair.begin(); } else { // we are already paired - handle_pair(true, this.public_key); + handle_pair(true); } } @@ -329,7 +347,7 @@ class Device : Object { warning("not paired and still got a packet, " + "assuming device is paired", Packet.PAIR); - handle_pair(true, ""); + handle_pair(true); } // emit signal @@ -348,22 +366,17 @@ class Device : Object { assert(pkt.pkt_type == Packet.PAIR); bool pair = pkt.body.get_boolean_member("pair"); - string public_key = ""; - if (pair) { - public_key = pkt.body.get_string_member("publicKey"); - } - handle_pair(pair, public_key); + handle_pair(pair); } /** * handle_pair: * @pair: pairing status - * @public_key: device public key * * Update device pair status. */ - private void handle_pair(bool pair, string public_key) { + private void handle_pair(bool pair) { if (this._pair_timeout_source != 0) { Source.remove(_pair_timeout_source); this._pair_timeout_source = 0; @@ -400,13 +413,6 @@ class Device : Object { } } - if (pair) { - // update public key - this.public_key = public_key; - } else { - this.public_key = ""; - } - // emit signal paired(is_paired); } diff --git a/src/mconnect/devicechannel.vala b/src/mconnect/devicechannel.vala index f7bb3f4..2237c27 100644 --- a/src/mconnect/devicechannel.vala +++ b/src/mconnect/devicechannel.vala @@ -32,10 +32,14 @@ class DeviceChannel : Object { public signal void packet_received(Packet pkt); private InetSocketAddress _isa = null; - private SocketConnection _conn = null; + private SocketConnection _sock_conn = null; + private TlsConnection _tls_conn = null; private DataOutputStream _dout = null; private DataInputStream _din = null; private uint _srcid = 0; + private Socket _socket = null; + + public TlsCertificate peer_certificate = null; // channel encryption method private Crypt _crypt = null; @@ -49,34 +53,7 @@ class DeviceChannel : Object { debug("channel destroyed"); } - public async bool open() { - GLib.assert(this._isa != null); - - debug("connect to %s:%u", _isa.address.to_string(), _isa.port); - - var client = new SocketClient(); - try { - _conn = yield client.connect_async(_isa); - } catch (Error e) { - // - warning("failed to connect to %s:%u: %s", - _isa.address.to_string(), _isa.port, - e.message); - // emit disconnected - return false; - } - - debug("connected to %s:%u", _isa.address.to_string(), _isa.port); - - // use data streams - _dout = new DataOutputStream(_conn.output_stream); - _din = new DataInputStream(_conn.input_stream); - // messages end with \n\n - _din.set_newline_type(DataStreamNewlineType.LF); - - // setup socket monitoring - Socket sock = _conn.get_socket(); - + private void fixup_socket(Socket sock) { #if 0 IPPROTO_TCP = 6, /* Transmission Control Protocol. */ @@ -107,24 +84,149 @@ class DeviceChannel : Object { // enable keepalive sock.set_keepalive(true); - // prep source for monitoring events - var source = sock.create_source(IOCondition.IN); + } + + private void replace_streams(InputStream input, OutputStream output) { + if (_dout != null) { + _dout.close(); + } + _dout = new DataOutputStream(output); + + if (_din != null) { + _din.close(); + } + _din = new DataInputStream(input); + // messages end with \n\n + _din.set_newline_type(DataStreamNewlineType.LF); + } + + private void monitor_events() { + var source = _socket.create_source(IOCondition.IN); source.set_callback((src, cond) => { return this._io_ready(cond); }); // attach source _srcid = source.attach(null); + } + + private void unmonitor_events() { + if (_srcid > 0) { + Source.remove(_srcid); + _srcid = 0; + } + } + + public async bool open() { + GLib.assert(this._isa != null); + + debug("connect to %s:%u", _isa.address.to_string(), _isa.port); + + var client = new SocketClient(); + SocketConnection conn; + try { + conn = yield client.connect_async(_isa); + } catch (Error e) { + // + warning("failed to connect to %s:%u: %s", + _isa.address.to_string(), _isa.port, + e.message); + // emit disconnected + return false; + } + + debug("connected to %s:%u", _isa.address.to_string(), _isa.port); + + _socket = conn.get_socket(); + + // fixup socket keepalive + fixup_socket(_socket); + + _sock_conn = conn; + + // input/output streams will close underlying base stream when .close() + // is called on them, make sure that we pass Unix*Stream with which can + // skip closing the socket + replace_streams(new UnixInputStream(_socket.fd, false), + new UnixOutputStream(_socket.fd, false)); + + // start monitoring socket events + monitor_events(); return true; } + /** + * secure: + * Switch channel to TLS mode + * + * When TLS was established, `peer_certificate` will store the remote client + * certificate. If `expected_peer` is null, the peer certificate will be + * accepted unconditionally during handshake and the caller must eventually + * decide if the client is to be trusted or not. However, if `expected_peer` + * was set, the received certificate and expected one will be compared + * during handshake and connection will be rejected if a mismatch is found. + * + * @param expected_peer the peer certificate we are expecting to see + * @return true if TLS negotiation was successful, false otherwise + */ + public async bool secure(TlsCertificate? expected_peer = null) { + GLib.assert(this._sock_conn != null); + + // stop monitoring socket events + unmonitor_events(); + + var cert = Core.get_certificate(); + + // wrap with TLS + var tls_conn = TlsServerConnection.@new(_sock_conn, cert); + tls_conn.authentication_mode = TlsAuthenticationMode.REQUESTED; + tls_conn.accept_certificate.connect((peer_cert, errors) => { + info("accept certificate, flags: 0x%x", errors); + info("certificate:\n%s\n", peer_cert.certificate_pem); + + this.peer_certificate = peer_cert; + + if (expected_peer != null) { + if (DebugLog.Verbose) { + vdebug("verify certificate, expecting: %s, got: %s", + expected_peer.certificate_pem, + peer_cert.certificate_pem); + } + + if (expected_peer.is_same(peer_cert)) { + return true; + } else { + warning("rejecting handshare, peer certificate mismatch, got:\n%s", + peer_cert.certificate_pem); + return false; + } + } + return true; + }); + + try { + info("attempt TLS handshake"); + var res = yield tls_conn.handshake_async(); + info("TLS handshare successful"); + } catch (Error e) { + warning("TLS handshake failed: %s", e.message); + return false; + } + + _tls_conn = tls_conn; + // data will now pass through TLS stream wrapper + replace_streams(_tls_conn.input_stream, + _tls_conn.output_stream); + + // monitor socket events + monitor_events(); + return true; + } + public void close() { debug("closing connection"); - if (_srcid > 0) { - Source.remove(_srcid); - _srcid = 0; - } + unmonitor_events(); try { if (_din != null) @@ -139,14 +241,24 @@ class DeviceChannel : Object { warning("failed to close data output: %s", e.message); } try { - if (_conn != null) - _conn.close(); + if (_tls_conn != null) + _tls_conn.close(); + } catch (Error e) { + warning("failed to close TLS connection: %s", e.message); + } + try { + if (_sock_conn != null) + _sock_conn.close(); } catch (Error e) { warning("failed to close connection: %s", e.message); } _din = null; _dout = null; - _conn = null; + _sock_conn = null; + _tls_conn = null; + _socket = null; + + this.peer_certificate = null; } /** @@ -158,7 +270,9 @@ class DeviceChannel : Object { public async void send(Packet pkt) { string to_send = pkt.to_string() + "\n"; debug("send data: %s", to_send); - // _dout.put_string(data); + + GLib.assert(_dout != null); + try { _dout.put_string(to_send); } catch (IOError e) { @@ -176,8 +290,11 @@ class DeviceChannel : Object { public bool receive() { size_t line_len; string data = null; - // read line up to newline + + GLib.assert(_din != null); + try { + // read line up to a newline data = _din.read_upto("\n", -1, out line_len, null); // expecting \n diff --git a/src/mconnect/packet.vala b/src/mconnect/packet.vala index efb413f..082b301 100644 --- a/src/mconnect/packet.vala +++ b/src/mconnect/packet.vala @@ -40,7 +40,7 @@ class Packet : GLib.Object { } } - public const int PROTOCOL_VERSION = 5; + public const int PROTOCOL_VERSION = 7; public const string IDENTITY = "kdeconnect.identity"; public const string PAIR = "kdeconnect.pair"; @@ -116,13 +116,11 @@ class Packet : GLib.Object { return null; } - public static Packet new_pair(string key, bool pair = true) { + public static Packet new_pair(bool pair = true) { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("pair"); builder.add_boolean_value(pair); - builder.set_member_name("publicKey"); - builder.add_string_value(key); builder.end_object(); var data_obj = builder.get_root().get_object();