Browse Source

device, channel, packet: support for TLS encrypted channel

Introduce support for device channel encryption using TLS. The change is
compatible with current KDE Connect protocol version 7.

Device no longer has a public key, instead the public key was replaced by its
certificate as obtained during TLS handshake.

Device channel connection is now 3 step - first, the initial connection happens,
after which an identity packet is sent to the device, followed by TLS
handshake (including crypto suite negotiation, certificate exchange). Note, KDE
Connect supports TLS 1.0 at the moment. Failure to perform a TLS handshake will
cause device connection to be dropped.
bboozzoo/tls-support
Maciek Borzecki 7 years ago
parent
commit
49fa30c841
5 changed files with 208 additions and 71 deletions
  1. +1
    -0
      Makefile.am
  2. +15
    -0
      src/mconnect/core.vala
  3. +33
    -27
      src/mconnect/device.vala
  4. +157
    -40
      src/mconnect/devicechannel.vala
  5. +2
    -4
      src/mconnect/packet.vala

+ 1
- 0
Makefile.am View File

@ -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 \


+ 15
- 0
src/mconnect/core.vala View File

@ -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;
}
}

+ 33
- 27
src/mconnect/device.vala View File

@ -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<string> _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<string>.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);
}


+ 157
- 40
src/mconnect/devicechannel.vala View File

@ -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


+ 2
- 4
src/mconnect/packet.vala View File

@ -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();


Loading…
Cancel
Save