2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-29 05:07:40 +00:00
Marko Zajc db615b82df Fix trusted networks
## Summary
1) Fix the code responsible for loading the trusted network list.
2) The old `#_#` delimiter has been replaced with a less likely NUL character. This requires re-adding trusted networks, so I can revert it if necessary.
3) Ignore incoming identity packets on untrusted devices if they come from an untrusted device.

BUG: 492302

## Test Plan

### Before:
1) Trusted networks were completely broken, an would show variations of `#` and `_` on the list due to a bug in the splitting code.
2) Any network with `#_#` in the SSID - although unlikely - would not be possible to use as a trusted network.
3) The device was still discoverable on an untrusted network by manually refreshing the devices list.

### After:
1) Trusted networks now load the SSID list correctly.
2) Networks with `#_#` in the SSID can be added as trusted networks.
3) The device is no longer discoverable on an untrusted network.
2024-09-21 22:24:46 +00:00

519 lines
20 KiB
Java

/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Backends.LanBackend;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Network;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.json.JSONException;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
import org.kde.kdeconnect.UserInterface.SettingsFragment;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import kotlin.text.Charsets;
/**
* This LanLinkProvider creates {@link LanLink}s to other devices on the same
* WiFi network. The first packet sent over a socket must be an
* {@link DeviceInfo#toIdentityPacket()}.
*
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
*/
public class LanLinkProvider extends BaseLinkProvider {
final static int UDP_PORT = 1716;
final static int MIN_PORT = 1716;
final static int MAX_PORT = 1764;
final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
final static int MAX_UDP_PACKET_SIZE = 1024 * 512;
final static long MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE = 500L;
private final Context context;
final HashMap<String, LanLink> visibleDevices = new HashMap<>(); // Links by device id
final ConcurrentHashMap<String, Long> lastConnectionTime = new ConcurrentHashMap<>();
private ServerSocket tcpServer;
private DatagramSocket udpServer;
private MdnsDiscovery mdnsDiscovery;
private long lastBroadcast = 0;
private final static long delayBetweenBroadcasts = 200;
private boolean listening = false;
public void onConnectionLost(BaseLink link) {
String deviceId = link.getDeviceId();
visibleDevices.remove(deviceId);
super.onConnectionLost(link);
}
//They received my UDP broadcast and are connecting to me. The first thing they send should be their identity packet.
@WorkerThread
private void tcpPacketReceived(Socket socket) throws IOException {
NetworkPacket networkPacket;
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message = reader.readLine();
networkPacket = NetworkPacket.unserialize(message);
//Log.e("TcpListener", "Received TCP packet: " + networkPacket.serialize());
} catch (Exception e) {
Log.e("KDE/LanLinkProvider", "Exception while receiving TCP packet", e);
return;
}
Log.i("KDE/LanLinkProvider", "identity packet received from a TCP connection from " + networkPacket.getString("deviceName"));
boolean deviceTrusted = isDeviceTrusted(networkPacket.getString("deviceId"));
if (!deviceTrusted && !TrustedNetworkHelper.isTrustedNetwork(context)) {
Log.i("KDE/LanLinkProvider", "Ignoring identity packet because the device is not trusted and I'm not on a trusted network.");
return;
}
identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally, deviceTrusted);
}
//I've received their broadcast and should connect to their TCP socket and send my identity.
@WorkerThread
private void udpPacketReceived(DatagramPacket packet) throws JSONException, IOException {
final InetAddress address = packet.getAddress();
String message = new String(packet.getData(), Charsets.UTF_8);
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
if (!DeviceInfo.isValidIdentityPacket(identityPacket)) {
Log.w("KDE/LanLinkProvider", "Invalid identity packet received.");
return;
}
final String deviceId = identityPacket.getString("deviceId");
String myId = DeviceHelper.getDeviceId(context);
if (deviceId.equals(myId)) {
//Ignore my own broadcast
return;
}
long now = System.currentTimeMillis();
Long last = lastConnectionTime.get(deviceId);
if (last != null && (last + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE > now)) {
Log.i("LanLinkProvider", "Discarding second UDP packet from the same device " + deviceId + " received too quickly");
return;
}
lastConnectionTime.put(deviceId, now);
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
if (tcpPort < MIN_PORT || tcpPort > MAX_PORT) {
Log.e("LanLinkProvider", "TCP port outside of kdeconnect's range");
return;
}
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
boolean deviceTrusted = isDeviceTrusted(identityPacket.getString("deviceId"));
if (!deviceTrusted && !TrustedNetworkHelper.isTrustedNetwork(context)) {
Log.i("KDE/LanLinkProvider", "Ignoring identity packet because the device is not trusted and I'm not on a trusted network.");
return;
}
SocketFactory socketFactory = SocketFactory.getDefault();
Socket socket = socketFactory.createSocket(address, tcpPort);
configureSocket(socket);
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket myIdentity = myDeviceInfo.toIdentityPacket();
OutputStream out = socket.getOutputStream();
out.write(myIdentity.serialize().getBytes());
out.flush();
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely, deviceTrusted);
}
private void configureSocket(Socket socket) {
try {
socket.setKeepAlive(true);
} catch (SocketException e) {
Log.e("LanLink", "Exception", e);
}
}
private boolean isDeviceTrusted(String deviceId) {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
return preferences.getBoolean(deviceId, false);
}
/**
* Called when a new 'identity' packet is received. Those are passed here by
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
* <p>
* Should be called on a new thread since it blocks until the handshake is completed.
* </p><p>
* If the remote device should be connected, this calls {@link #addLink}.
* Otherwise, if there was an Exception, we unpair from that device.
* </p>
*
* @param identityPacket identity of a remote device
* @param socket a new Socket, which should be used to receive packets from the remote device
* @param connectionStarted which side started this connection
* @param deviceTrusted whether the packet comes from a trusted device
*/
@WorkerThread
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted, final boolean deviceTrusted) throws IOException {
if (!DeviceInfo.isValidIdentityPacket(identityPacket)) {
Log.w("KDE/LanLinkProvider", "Invalid identity packet received.");
return;
}
String myId = DeviceHelper.getDeviceId(context);
final String deviceId = identityPacket.getString("deviceId");
if (deviceId.equals(myId)) {
Log.e("KDE/LanLinkProvider", "Somehow I'm connected to myself, ignoring. This should not happen.");
return;
}
// If I'm the TCP server I will be the SSL client and viceversa.
final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
if (deviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
//Device paired with and old version, we can't use it as we lack the certificate
Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device == null) {
return;
}
device.unpair();
//Retry as unpaired
identityPacketReceived(identityPacket, socket, connectionStarted, deviceTrusted);
}
String deviceName = identityPacket.getString("deviceName", "unknown");
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + deviceTrusted);
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, deviceTrusted, clientMode);
sslSocket.addHandshakeCompletedListener(event -> {
String mode = clientMode ? "client" : "server";
try {
Certificate certificate = event.getPeerCertificates()[0];
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
addOrUpdateLink(sslSocket, deviceInfo);
} catch (IOException e) {
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device == null) {
return;
}
device.unpair();
}
});
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
Log.d("LanLinkProvider", "Starting handshake");
sslSocket.startHandshake();
Log.d("LanLinkProvider", "Handshake done");
}
/**
* Add or update a link in the {@link #visibleDevices} map.
*
* @param socket a new Socket, which should be used to send and receive packets from the remote device
* @param deviceInfo remote device info
* @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket, DeviceInfo)}
*/
private void addOrUpdateLink(SSLSocket socket, DeviceInfo deviceInfo) throws IOException {
LanLink link = visibleDevices.get(deviceInfo.id);
if (link != null) {
if (!link.getDeviceInfo().certificate.equals(deviceInfo.certificate)) {
Log.e("LanLinkProvider", "LanLink was asked to replace a socket but the certificate doesn't match, aborting");
return;
}
// Update existing link
Log.d("KDE/LanLinkProvider", "Reusing same link for device " + deviceInfo.id);
link.reset(socket, deviceInfo);
onDeviceInfoUpdated(deviceInfo);
} else {
// Create a new link
Log.d("KDE/LanLinkProvider", "Creating a new link for device " + deviceInfo.id);
link = new LanLink(context, deviceInfo, this, socket);
visibleDevices.put(deviceInfo.id, link);
onConnectionReceived(link);
}
}
public LanLinkProvider(Context context) {
this.context = context;
this.mdnsDiscovery = new MdnsDiscovery(context, this);
}
private void setupUdpListener() {
try {
udpServer = new DatagramSocket(null);
udpServer.setReuseAddress(true);
udpServer.setBroadcast(true);
} catch (SocketException e) {
Log.e("LanLinkProvider", "Error creating udp server", e);
throw new RuntimeException(e);
}
try {
udpServer.bind(new InetSocketAddress(UDP_PORT));
} catch (SocketException e) {
// We ignore this exception and continue without being able to receive broadcasts instead of crashing the app.
Log.e("LanLinkProvider", "Error binding udp server. We can send udp broadcasts but not receive them", e);
}
ThreadHelper.execute(() -> {
Log.i("UdpListener", "Starting UDP listener");
while (listening) {
try {
DatagramPacket packet = new DatagramPacket(new byte[MAX_UDP_PACKET_SIZE], MAX_UDP_PACKET_SIZE);
udpServer.receive(packet);
ThreadHelper.execute(() -> {
try {
udpPacketReceived(packet);
} catch (JSONException | IOException e) {
Log.e("LanLinkProvider", "Exception receiving incoming UDP connection", e);
}
});
} catch (IOException e) {
Log.e("LanLinkProvider", "UdpReceive exception", e);
onNetworkChange(null); // Trigger a UDP broadcast to try to get them to connect to us instead
}
}
Log.w("UdpListener", "Stopping UDP listener");
});
}
private void setupTcpListener() {
try {
tcpServer = openServerSocketOnFreePort(MIN_PORT);
} catch (IOException e) {
Log.e("LanLinkProvider", "Error creating tcp server", e);
throw new RuntimeException(e);
}
ThreadHelper.execute(() -> {
while (listening) {
try {
Socket socket = tcpServer.accept();
configureSocket(socket);
ThreadHelper.execute(() -> {
try {
tcpPacketReceived(socket);
} catch (IOException e) {
Log.e("LanLinkProvider", "Exception receiving incoming TCP connection", e);
}
});
} catch (Exception e) {
Log.e("LanLinkProvider", "TcpReceive exception", e);
}
}
Log.w("TcpListener", "Stopping TCP listener");
});
}
static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException {
int tcpPort = minPort;
while (tcpPort <= MAX_PORT) {
try {
ServerSocket candidateServer = new ServerSocket();
candidateServer.bind(new InetSocketAddress(tcpPort));
Log.i("KDE/LanLink", "Using port " + tcpPort);
return candidateServer;
} catch (IOException e) {
tcpPort++;
if (tcpPort == MAX_PORT) {
Log.e("KDE/LanLink", "No ports available");
throw e; //Propagate exception
}
}
}
throw new RuntimeException("This should not be reachable");
}
private void broadcastUdpIdentityPacket(@Nullable Network network) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
if (!preferences.getBoolean(SettingsFragment.KEY_UDP_BROADCAST_ENABLED, true)) {
Log.i("LanLinkProvider", "UDP broadcast is disabled in settings. Skipping.");
return;
}
ThreadHelper.execute(() -> {
List<String> ipStringList = CustomDevicesActivity
.getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
if (TrustedNetworkHelper.isTrustedNetwork(context)) {
ipStringList.add("255.255.255.255"); //Default: broadcast.
} else {
Log.i("LanLinkProvider", "Current network isn't trusted, not broadcasting");
}
ArrayList<InetAddress> ipList = new ArrayList<>();
for (String ip : ipStringList) {
try {
ipList.add(InetAddress.getByName(ip));
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
if (ipList.isEmpty()) {
return;
}
sendUdpIdentityPacket(ipList, network);
});
}
@WorkerThread
public void sendUdpIdentityPacket(List<InetAddress> ipList, @Nullable Network network) {
if (tcpServer == null || !tcpServer.isBound()) {
Log.i("LanLinkProvider", "Won't broadcast UDP packet if TCP socket is not ready yet");
return;
}
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket identity = myDeviceInfo.toIdentityPacket();
identity.set("tcpPort", tcpServer.getLocalPort());
byte[] bytes;
try {
bytes = identity.serialize().getBytes(Charsets.UTF_8);
} catch (JSONException e) {
Log.e("KDE/LanLinkProvider", "Failed to serialize identity packet", e);
return;
}
DatagramSocket socket;
try {
socket = new DatagramSocket();
if (network != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
try {
network.bindSocket(socket);
} catch (IOException e) {
Log.w("LanLinkProvider", "Couldn't bind socket to the network");
e.printStackTrace();
}
}
socket.setReuseAddress(true);
socket.setBroadcast(true);
} catch (SocketException e) {
Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e);
return;
}
for (InetAddress ip : ipList) {
try {
socket.send(new DatagramPacket(bytes, bytes.length, ip, MIN_PORT));
//Log.i("KDE/LanLinkProvider","Udp identity packet sent to address "+client);
} catch (IOException e) {
Log.e("KDE/LanLinkProvider", "Sending udp identity packet failed. Invalid address? (" + ip.toString() + ")", e);
}
}
socket.close();
}
@Override
public void onStart() {
//Log.i("KDE/LanLinkProvider", "onStart");
if (!listening) {
listening = true;
setupUdpListener();
setupTcpListener();
mdnsDiscovery.startDiscovering();
mdnsDiscovery.startAnnouncing();
broadcastUdpIdentityPacket(null);
}
}
@Override
public void onNetworkChange(@Nullable Network network) {
if (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) {
Log.i("LanLinkProvider", "onNetworkChange: relax cowboy");
return;
}
lastBroadcast = System.currentTimeMillis();
broadcastUdpIdentityPacket(network);
mdnsDiscovery.stopDiscovering();
mdnsDiscovery.startDiscovering();
}
@Override
public void onStop() {
//Log.i("KDE/LanLinkProvider", "onStop");
listening = false;
mdnsDiscovery.stopAnnouncing();
mdnsDiscovery.stopDiscovering();
try {
tcpServer.close();
} catch (Exception e) {
Log.e("LanLink", "Exception", e);
}
try {
udpServer.close();
} catch (Exception e) {
Log.e("LanLink", "Exception", e);
}
}
@Override
public String getName() {
return "LanLinkProvider";
}
@Override
public int getPriority() { return 20; }
}