2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 14:15:14 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Albert Vaca Cintora
ee806f2a22 Release 1.26.0 beta2 2023-06-04 23:54:33 +02:00
Albert Vaca Cintora
5a620b4a7a Start the timer before the packet has been sent
Fixes the loopback provider starting the timer after the paring is
already done, causing it to unpair after 30 seconds.
2023-06-04 20:12:44 +02:00
Albert Vaca Cintora
e9732d009a Add missing cancelTimer 2023-06-04 20:08:18 +02:00
Albert Vaca Cintora
2386c9cb48 More logging 2023-06-04 20:06:22 +02:00
Albert Vaca Cintora
5384cb18a6 Add missing break caught in code review 2023-06-04 20:06:22 +02:00
Albert Vaca Cintora
9e958b23f4 Remove loopback link provider left in by mistake 2023-06-04 20:06:22 +02:00
Albert Vaca Cintora
6a3d4de995 Release 1.26.0 beta 2023-06-04 20:06:21 +02:00
Albert Vaca Cintora
a1ccc7b64e Fail earlier if we don't have a certificate 2023-06-04 20:06:21 +02:00
Albert Vaca Cintora
d0923b845b Have a single PairingHandler for all links 2023-06-04 20:06:21 +02:00
Albert Vaca Cintora
0eb4b5bced Replace DeviceNames library
The version of the library we used stopped working in 2020 when the names
database it tries to download got deleted from the master branch of their
Github repo. There's a newer version, but it seems to have lost the
fetch-from-the-internet functionality (it only bundles a list of names) and
for some reason it crashes when I tested it (I've opened an issue on their
repo). Since Google now provides a CSV with all the Android device names
that exist, I've replaced the library by my own function that downloads the
CSV file (~3MB) in the first run of the app and looks for the name there.
2023-06-04 20:06:21 +02:00
23 changed files with 487 additions and 897 deletions

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.kde.kdeconnect_tp"
android:versionCode="12500"
android:versionName="1.25.0">
android:versionCode="12591"
android:versionName="1.26.0 beta2">
<uses-feature
android:name="android.hardware.telephony"

View File

@@ -180,7 +180,6 @@ dependencies {
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps
implementation 'com.jaredrummler:android-device-names:1.1.9' //To get a human-friendly device name
implementation 'org.apache.sshd:sshd-core:0.14.0'
implementation 'org.apache.mina:mina-core:2.0.19' //For some reason, makes sshd-core:0.14.0 work without NIO, which isn't available until Android 8 (api 26)
@@ -203,6 +202,8 @@ dependencies {
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.univocity:univocity-parsers:2.9.1'
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"

View File

@@ -30,14 +30,13 @@ public abstract class BaseLink {
private final ArrayList<PacketReceiver> receivers = new ArrayList<>();
protected BaseLink(@NonNull Context context, @NonNull String deviceId, @NonNull BaseLinkProvider linkProvider) {
this.context = context;
this.context = context;
this.linkProvider = linkProvider;
this.deviceId = deviceId;
}
/* To be implemented by each link for pairing handlers */
public abstract String getName();
public abstract BasePairingHandler getPairingHandler(@NonNull Device device, @NonNull BasePairingHandler.PairingHandlerCallback callback);
public String getDeviceId() {
return deviceId;

View File

@@ -6,8 +6,11 @@
package org.kde.kdeconnect.Backends;
import androidx.annotation.NonNull;
import org.kde.kdeconnect.NetworkPacket;
import java.security.cert.Certificate;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class BaseLinkProvider {
@@ -15,7 +18,10 @@ public abstract class BaseLinkProvider {
private final CopyOnWriteArrayList<ConnectionReceiver> connectionReceivers = new CopyOnWriteArrayList<>();
public interface ConnectionReceiver {
void onConnectionReceived(NetworkPacket identityPacket, BaseLink link);
void onConnectionReceived(@NonNull final String deviceId,
@NonNull final Certificate certificate,
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link);
void onConnectionLost(BaseLink link);
}
@@ -28,10 +34,13 @@ public abstract class BaseLinkProvider {
}
//These two should be called when the provider links to a new computer
protected void connectionAccepted(NetworkPacket identityPacket, BaseLink link) {
protected void connectionAccepted(@NonNull final String deviceId,
@NonNull final Certificate certificate,
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link) {
//Log.i("KDE/LinkProvider", "connectionAccepted");
for(ConnectionReceiver cr : connectionReceivers) {
cr.onConnectionReceived(identityPacket, link);
cr.onConnectionReceived(deviceId, certificate, identityPacket, link);
}
}
protected void connectionLost(BaseLink link) {
@@ -45,8 +54,6 @@ public abstract class BaseLinkProvider {
public abstract void onStart();
public abstract void onStop();
public abstract void onNetworkChange();
//public abstract int getPriority();
public abstract String getName();
}

View File

@@ -1,68 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Backends;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
/**
* This class separates the pairing interface for each type of link.
* Since different links can pair via different methods, like for LanLink certificate and public key should be shared,
* for Bluetooth link they should be paired via bluetooth etc.
* Each "Device" instance maintains a hash map for these pairing handlers so that there can be single pairing handler per
* per link type per device.
* Pairing handler keeps information about device, latest link, and pair status of the link
* During first pairing process, the pairing process is nearly same as old process.
* After that if any one of the link is paired, then we can say that device is paired, so new link will pair automatically
*/
public abstract class BasePairingHandler {
protected enum PairStatus{
NotPaired,
Requested,
RequestedByPeer,
Paired
}
public interface PairingHandlerCallback {
void incomingRequest();
void pairingDone();
void pairingFailed(String error);
void unpaired();
}
protected final Device mDevice;
protected PairStatus mPairStatus;
protected final PairingHandlerCallback mCallback;
protected BasePairingHandler(Device device, PairingHandlerCallback callback) {
this.mDevice = device;
this.mCallback = callback;
}
protected boolean isPaired() {
return mPairStatus == PairStatus.Paired;
}
public boolean isPairRequested() {
return mPairStatus == PairStatus.Requested;
}
public boolean isPairRequestedByPeer() {
return mPairStatus == PairStatus.RequestedByPeer;
}
/* To be implemented by respective pairing handler */
public abstract void packetReceived(NetworkPacket np);
public abstract void requestPairing();
public abstract void acceptPairing();
public abstract void cancelPairing();
public abstract void unpair();
}

View File

@@ -16,7 +16,6 @@ import androidx.annotation.WorkerThread;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
@@ -112,11 +111,6 @@ public class BluetoothLink extends BaseLink {
return "BluetoothLink";
}
@Override
public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) {
return new BluetoothPairingHandler(device, callback);
}
public void disconnect() {
if (connection == null) {
return;

View File

@@ -15,10 +15,12 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.NetworkPacket;
@@ -27,6 +29,8 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -48,8 +52,12 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
private ServerRunnable serverRunnable;
private ClientRunnable clientRunnable;
private void addLink(NetworkPacket identityPacket, BluetoothLink link) {
private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException {
String deviceId = identityPacket.getString("deviceId");
String certificateString = identityPacket.getString("certificate");
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
Log.i("BluetoothLinkProvider", "addLink to " + deviceId);
BluetoothLink oldLink = visibleComputers.get(deviceId);
if (oldLink == link) {
@@ -57,7 +65,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
return;
}
visibleComputers.put(deviceId, link);
connectionAccepted(identityPacket, link);
connectionAccepted(deviceId, certificate, identityPacket, link);
link.startListening();
if (oldLink != null) {
Log.i("BluetoothLinkProvider", "Removing old connection to same device");
@@ -189,6 +197,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
InputStream inputStream = connection.getDefaultInputStream();
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
byte[] message = np.serialize().getBytes(Charsets.UTF_8);
outputStream.write(message);
outputStream.flush();
@@ -371,7 +380,11 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
link.sendPacket(np2, new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
addLink(identityPacket, link);
try {
addLink(identityPacket, link);
} catch (CertificateException e) {
e.printStackTrace();
}
}
@Override

View File

@@ -1,181 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Backends.BluetoothBackend;
import android.util.Log;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect_tp.R;
import java.util.Timer;
import java.util.TimerTask;
public class BluetoothPairingHandler extends BasePairingHandler {
private Timer mPairingTimer;
public BluetoothPairingHandler(Device device, final PairingHandlerCallback callback) {
super(device, callback);
if (device.isPaired()) {
mPairStatus = PairStatus.Paired;
} else {
mPairStatus = PairStatus.NotPaired;
}
}
// @Override
private NetworkPacket createPairPacket() {
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", true);
return np;
}
@Override
public void packetReceived(NetworkPacket np) {
boolean wantsPair = np.getBoolean("pair");
if (wantsPair == isPaired()) {
if (mPairStatus == PairStatus.Requested) {
//Log.e("Device","Unpairing (pair rejected)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
}
return;
}
if (wantsPair) {
if (mPairStatus == PairStatus.Requested) { //We started pairing
hidePairingNotification();
pairingDone();
} else {
// If device is already paired, accept pairing silently
if (mDevice.isPaired()) {
acceptPairing();
return;
}
// Pairing notifications are still managed by device as there is no other way to
// know about notificationId to cancel notification when PairActivity is started
// Even putting notificationId in intent does not work because PairActivity can be
// started from MainActivity too, so then notificationId cannot be set
hidePairingNotification();
mDevice.displayPairingNotification();
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.w("KDE/Device", "Unpairing (timeout B)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
}
}, 25 * 1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds)
mPairStatus = PairStatus.RequestedByPeer;
mCallback.incomingRequest();
}
} else {
Log.i("KDE/Pairing", "Unpair request");
if (mPairStatus == PairStatus.Requested) {
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
} else if (mPairStatus == PairStatus.Paired) {
mCallback.unpaired();
}
mPairStatus = PairStatus.NotPaired;
}
}
@Override
public void requestPairing() {
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
hidePairingNotification(); //Will stop the pairingTimer if it was running
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out));
Log.w("KDE/Device", "Unpairing (timeout A)");
mPairStatus = PairStatus.NotPaired;
}
}, 30 * 1000); //Time to wait for the other to accept
mPairStatus = PairStatus.Requested;
}
@Override
public void onFailure(Throwable e) {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.runcommand_notreachable));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
private void hidePairingNotification() {
mDevice.hidePairingNotification();
if (mPairingTimer != null) {
mPairingTimer.cancel();
}
}
@Override
public void acceptPairing() {
hidePairingNotification();
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
pairingDone();
}
@Override
public void onFailure(Throwable e) {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
@Override
public void cancelPairing() {
hidePairingNotification();
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
//@Override
private void pairingDone() {
// Store device information needed to create a Device object in a future
//Log.e("KDE/PairingDone", "Pairing Done");
mPairStatus = PairStatus.Paired;
mCallback.pairingDone();
}
@Override
public void unpair() {
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
}

View File

@@ -14,7 +14,6 @@ import androidx.annotation.WorkerThread;
import org.json.JSONObject;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper;
@@ -117,11 +116,6 @@ public class LanLink extends BaseLink {
return "LanLink";
}
@Override
public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) {
return new LanPairingHandler(device, callback);
}
//Blocking, do not call from main thread
@WorkerThread
@Override

View File

@@ -9,7 +9,6 @@ package org.kde.kdeconnect.Backends.LanBackend;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
@@ -212,9 +211,8 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
String mode = clientMode ? "client" : "server";
try {
Certificate certificate = event.getPeerCertificates()[0];
identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0));
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite());
addLink(identityPacket, sslsocket);
addLink(deviceId, certificate, identityPacket, sslsocket);
} catch (Exception e) {
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e);
Device device = KdeConnect.getInstance().getDevice(deviceId);
@@ -253,14 +251,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
* {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27).
* </p>
*
* @param identityPacket representation of remote device
* @param socket a new Socket, which should be used to receive packets from the remote device
* @param connectionOrigin which side started this connection
* @param deviceId remote device id
* @param certificate remote device certificate
* @param identityPacket identity packet with the remote device's device name, type, protocol version, etc.
* @param socket a new Socket, which should be used to send and receive packets from the remote device
* @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket, LanLink.ConnectionStarted)}
*/
private void addLink(final NetworkPacket identityPacket, SSLSocket socket) throws IOException {
String deviceId = identityPacket.getString("deviceId");
private void addLink(String deviceId, Certificate certificate, final NetworkPacket identityPacket, SSLSocket socket) throws IOException {
LanLink currentLink = visibleComputers.get(deviceId);
if (currentLink != null) {
//Update old link
@@ -272,7 +269,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
//Let's create the link
LanLink link = new LanLink(context, deviceId, this, socket);
visibleComputers.put(deviceId, link);
connectionAccepted(identityPacket, link);
connectionAccepted(deviceId, certificate, identityPacket, link);
}
}

View File

@@ -1,202 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@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.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect_tp.R;
import java.security.cert.CertificateEncodingException;
import java.util.Timer;
import java.util.TimerTask;
public class LanPairingHandler extends BasePairingHandler {
private Timer mPairingTimer;
public LanPairingHandler(Device device, final PairingHandlerCallback callback) {
super(device, callback);
if (device.isPaired()) {
mPairStatus = PairStatus.Paired;
} else {
mPairStatus = PairStatus.NotPaired;
}
}
private NetworkPacket createPairPacket() {
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", true);
return np;
}
@Override
public void packetReceived(NetworkPacket np) {
boolean wantsPair = np.getBoolean("pair");
if (wantsPair == isPaired()) {
if (mPairStatus == PairStatus.Requested || mPairStatus == PairStatus.RequestedByPeer) {
//Log.e("Device","Unpairing (pair rejected)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
}
return;
}
if (wantsPair) {
if (mPairStatus == PairStatus.Requested) { //We started pairing
hidePairingNotification();
pairingDone();
} else {
// If device is already paired, accept pairing silently
if (mDevice.isPaired()) {
acceptPairing();
return;
}
// Pairing notifications are still managed by device as there is no other way to
// know about notificationId to cancel notification when PairActivity is started
// Even putting notificationId in intent does not work because PairActivity can be
// started from MainActivity too, so then notificationId cannot be set
hidePairingNotification();
mDevice.displayPairingNotification();
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.w("KDE/Device","Unpairing (timeout B)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
}
}, 25*1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds)
mPairStatus = PairStatus.RequestedByPeer;
mCallback.incomingRequest();
}
} else {
Log.i("KDE/Pairing", "Unpair request");
if (mPairStatus == PairStatus.Requested) {
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
} else if (mPairStatus == PairStatus.Paired) {
mCallback.unpaired();
}
mPairStatus = PairStatus.NotPaired;
}
}
@Override
public void requestPairing() {
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
hidePairingNotification(); //Will stop the pairingTimer if it was running
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out));
Log.w("KDE/Device","Unpairing (timeout A)");
mPairStatus = PairStatus.NotPaired;
}
}, 30*1000); //Time to wait for the other to accept
mPairStatus = PairStatus.Requested;
}
@Override
public void onFailure(Throwable e) {
Log.e("LanPairing/onFailure", "Exception", e);
mCallback.pairingFailed(mDevice.getContext().getString(R.string.runcommand_notreachable));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
private void hidePairingNotification() {
mDevice.hidePairingNotification();
if (mPairingTimer != null) {
mPairingTimer .cancel();
}
}
@Override
public void acceptPairing() {
hidePairingNotification();
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
pairingDone();
}
@Override
public void onFailure(Throwable e) {
Log.e("LanPairing/onFailure", "Exception", e);
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
@Override
public void cancelPairing() {
hidePairingNotification();
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
private void pairingDone() {
// Store device information needed to create a Device object in a future
//Log.e("KDE/PairingDone", "Pairing Done");
SharedPreferences.Editor editor = mDevice.getContext().getSharedPreferences(mDevice.getDeviceId(), Context.MODE_PRIVATE).edit();
try {
String encodedCertificate = Base64.encodeToString(mDevice.certificate.getEncoded(), 0);
editor.putString("certificate", encodedCertificate);
} catch (NullPointerException n) {
Log.w("KDE/PairingDone", "Certificate is null, remote device does not support ssl", n);
} catch (CertificateEncodingException c) {
Log.e("KDE/PairingDOne", "Error encoding certificate", c);
} catch (Exception e) {
Log.e("KDE/Pairng", "Exception", e);
}
editor.apply();
mPairStatus = PairStatus.Paired;
mCallback.pairingDone();
}
@Override
public void unpair() {
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
}

View File

@@ -13,7 +13,6 @@ import androidx.annotation.WorkerThread;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
@@ -28,11 +27,6 @@ public class LoopbackLink extends BaseLink {
return "LoopbackLink";
}
@Override
public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) {
return new LoopbackPairingHandler(device, callback);
}
@WorkerThread
@Override
public boolean sendPacket(@NonNull NetworkPacket in, @NonNull Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {

View File

@@ -9,6 +9,8 @@ package org.kde.kdeconnect.Backends.LoopbackBackend;
import android.content.Context;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.NetworkPacket;
public class LoopbackLinkProvider extends BaseLinkProvider {
@@ -31,14 +33,10 @@ public class LoopbackLinkProvider extends BaseLinkProvider {
@Override
public void onNetworkChange() {
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
connectionAccepted(np, new LoopbackLink(context, this));
String deviceId = DeviceHelper.getDeviceId(context);
connectionAccepted(deviceId, SslHelper.certificate, np, new LoopbackLink(context, this));
}
/*
@Override
public int getPriority() {
return 0;
}
*/
@Override
public String getName() {
return "LoopbackLinkProvider";

View File

@@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Backends.LoopbackBackend;
import android.util.Log;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
public class LoopbackPairingHandler extends BasePairingHandler {
public LoopbackPairingHandler(Device device, PairingHandlerCallback callback) {
super(device, callback);
}
@Override
public void packetReceived(NetworkPacket np) {
}
@Override
public void requestPairing() {
Log.i("LoopbackPairing", "requestPairing");
mCallback.pairingDone();
}
@Override
public void acceptPairing() {
Log.i("LoopbackPairing", "acceptPairing");
mCallback.pairingDone();
}
@Override
public void cancelPairing() {
Log.i("LoopbackPairing", "cancelPairing");
mCallback.unpaired();
}
@Override
public void unpair() {
Log.i("LoopbackPairing", "unpair");
mCallback.unpaired();
}
}

View File

@@ -28,23 +28,23 @@ import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.apache.commons.lang3.StringUtils;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.PairingHandler;
import org.kde.kdeconnect_tp.R;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
@@ -59,24 +59,17 @@ public class Device implements BaseLink.PacketReceiver {
public Certificate certificate;
private int notificationId;
private int protocolVersion;
private DeviceType deviceType;
private PairStatus pairStatus;
private final CopyOnWriteArrayList<PairingCallback> pairingCallback = new CopyOnWriteArrayList<>();
private final Map<String, BasePairingHandler> pairingHandlers = new HashMap<>();
PairingHandler pairingHandler;
private final CopyOnWriteArrayList<PairingHandler.PairingCallback> pairingCallbacks = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>();
private DevicePacketQueue packetQueue;
private List<String> supportedPlugins = new ArrayList<>();
private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>();
private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>();
private final SharedPreferences settings;
private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();
private Set<String> incomingCapabilities = new HashSet<>();
@@ -92,11 +85,6 @@ public class Device implements BaseLink.PacketReceiver {
void onPluginsChanged(@NonNull Device device);
}
public enum PairStatus {
NotPaired,
Paired
}
public enum DeviceType {
Phone,
Tablet,
@@ -143,28 +131,21 @@ public class Device implements BaseLink.PacketReceiver {
}
}
public interface PairingCallback {
void incomingRequest();
void pairingSuccessful();
void pairingFailed(String error);
void unpaired();
}
//Remembered trusted device, we need to wait for a incoming devicelink to communicate
Device(Context context, String deviceId) {
settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
//Log.e("Device","Constructor A");
// Remembered trusted device, we need to wait for a incoming Link to communicate
Device(@NonNull Context context, @NonNull String deviceId) throws CertificateException {
this.context = context;
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired);
this.deviceId = deviceId;
this.name = settings.getString("deviceName", context.getString(R.string.unknown_device));
this.pairStatus = PairStatus.Paired;
this.protocolVersion = DeviceHelper.ProtocolVersion; //We don't know it yet
this.protocolVersion = 0; //We don't know it yet
this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop"));
this.certificate = SslHelper.getDeviceCertificate(context, deviceId);
Log.i("Device","Loading trusted device: " + this.name);
//Assume every plugin is supported until addLink is called and we can get the actual list
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
@@ -173,21 +154,24 @@ public class Device implements BaseLink.PacketReceiver {
//reloadPluginsFromSettings();
}
//Device known via an incoming connection sent to us via a devicelink, we know everything but we don't trust it yet
Device(Context context, NetworkPacket np, BaseLink dl) {
//Log.e("Device","Constructor B");
// Device known via an incoming connection sent to us via a Link, we don't trust it yet
Device(@NonNull Context context, @NonNull String deviceId, @NonNull Certificate certificate, @NonNull NetworkPacket identityPacket, @NonNull BaseLink dl) {
Log.i("Device","Creating untrusted device");
this.context = context;
this.deviceId = np.getString("deviceId");
this.name = context.getString(R.string.unknown_device); //We read it in addLink
this.pairStatus = PairStatus.NotPaired;
this.protocolVersion = 0;
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired);
this.deviceId = deviceId;
this.certificate = certificate;
// The following properties are read from the identityPacket in addLink since they can change in future identity packets
this.name = context.getString(R.string.unknown_device);
this.deviceType = DeviceType.Computer;
this.protocolVersion = 0;
settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
addLink(np, dl);
addLink(identityPacket, dl);
}
public String getName() {
@@ -221,144 +205,105 @@ public class Device implements BaseLink.PacketReceiver {
//
public boolean isPaired() {
return pairStatus == PairStatus.Paired;
return pairingHandler.getState() == PairingHandler.PairState.Paired;
}
/* Asks all pairing handlers that, is pair requested? */
public boolean isPairRequested() {
for (BasePairingHandler ph : pairingHandlers.values()) {
if (ph.isPairRequested()) {
return true;
}
}
return false;
return pairingHandler.getState() == PairingHandler.PairState.Requested;
}
/* Asks all pairing handlers that, is pair requested by peer? */
public boolean isPairRequestedByPeer() {
for (BasePairingHandler ph : pairingHandlers.values()) {
if (ph.isPairRequestedByPeer()) {
return true;
}
}
return false;
return pairingHandler.getState() == PairingHandler.PairState.RequestedByPeer;
}
public void addPairingCallback(PairingCallback callback) {
pairingCallback.add(callback);
public void addPairingCallback(PairingHandler.PairingCallback callback) {
pairingCallbacks.add(callback);
}
public void removePairingCallback(PairingCallback callback) {
pairingCallback.remove(callback);
public void removePairingCallback(PairingHandler.PairingCallback callback) {
pairingCallbacks.remove(callback);
}
public void requestPairing() {
Resources res = context.getResources();
if (isPaired()) {
for (PairingCallback cb : pairingCallback) {
cb.pairingFailed(res.getString(R.string.error_already_paired));
}
return;
}
if (!isReachable()) {
for (PairingCallback cb : pairingCallback) {
cb.pairingFailed(res.getString(R.string.error_not_reachable));
}
return;
}
for (BasePairingHandler ph : pairingHandlers.values()) {
ph.requestPairing();
}
pairingHandler.requestPairing();
}
public void unpair() {
for (BasePairingHandler ph : pairingHandlers.values()) {
ph.unpair();
}
unpairInternal(); // Even if there are no pairing handlers, unpair
}
/**
* This method does not send an unpair packet, instead it unpairs internally by deleting trusted device info.
* Likely to be called after sending packet from pairing handler
*/
private void unpairInternal() {
//Log.e("Device","Unpairing (unpairInternal)");
pairStatus = PairStatus.NotPaired;
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().remove(deviceId).apply();
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
devicePreferences.edit().clear().apply();
for (PairingCallback cb : pairingCallback) cb.unpaired();
reloadPluginsFromSettings();
}
/* This method should be called after pairing is done from pairing handler. Calling this method again should not create any problem as most of the things will get over writter*/
private void pairingDone() {
//Log.e("Device", "Storing as trusted, deviceId: "+deviceId);
hidePairingNotification();
pairStatus = PairStatus.Paired;
//Store as trusted device
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().putBoolean(deviceId, true).apply();
SharedPreferences.Editor editor = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE).edit();
editor.putString("deviceName", name);
editor.putString("deviceType", deviceType.toString());
editor.apply();
reloadPluginsFromSettings();
for (PairingCallback cb : pairingCallback) {
cb.pairingSuccessful();
}
pairingHandler.unpair();
}
/* This method is called after accepting pair request form GUI */
public void acceptPairing() {
Log.i("KDE/Device", "Accepted pair request started by the other device");
for (BasePairingHandler ph : pairingHandlers.values()) {
ph.acceptPairing();
}
pairingHandler.acceptPairing();
}
/* This method is called after rejecting pairing from GUI */
public void cancelPairing() {
Log.i("KDE/Device", "This side cancelled the pair request");
pairStatus = PairStatus.NotPaired;
for (BasePairingHandler ph : pairingHandlers.values()) {
ph.cancelPairing();
}
for (PairingCallback cb : pairingCallback) {
cb.pairingFailed(context.getString(R.string.error_canceled_by_user));
}
pairingHandler.cancelPairing();
}
PairingHandler.PairingCallback pairingCallback = new PairingHandler.PairingCallback() {
@Override
public void incomingPairRequest() {
displayPairingNotification();
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
cb.incomingPairRequest();
}
}
@Override
public void pairingSuccessful() {
hidePairingNotification();
// Store current device certificate so we can check it in the future (TOFU)
SharedPreferences.Editor editor = context.getSharedPreferences(getDeviceId(), Context.MODE_PRIVATE).edit();
try {
String encodedCertificate = Base64.encodeToString(certificate.getEncoded(), 0);
editor.putString("certificate", encodedCertificate);
} catch(CertificateEncodingException e) {
throw new RuntimeException(e);
}
editor.putString("deviceName", name);
editor.putString("deviceType", deviceType.toString());
editor.apply();
// Store as trusted device
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().putBoolean(deviceId, true).apply();
reloadPluginsFromSettings();
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
cb.pairingSuccessful();
}
}
@Override
public void pairingFailed(String error) {
hidePairingNotification();
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
cb.pairingFailed(error);
}
}
@Override
public void unpaired() {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().remove(deviceId).apply();
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
devicePreferences.edit().clear().apply();
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
cb.unpaired();
}
reloadPluginsFromSettings();
}
};
//
// Notification related methods used during pairing
//
@@ -414,7 +359,7 @@ public class Device implements BaseLink.PacketReceiver {
}
//
// ComputerLink-related functions
// Link-related functions
//
public boolean isReachable() {
@@ -442,54 +387,11 @@ public class Device implements BaseLink.PacketReceiver {
this.deviceType = DeviceType.FromString(identityPacket.getString("deviceType", "desktop"));
}
if (identityPacket.has("certificate")) {
String certificateString = identityPacket.getString("certificate");
try {
byte[] certificateBytes = Base64.decode(certificateString, 0);
certificate = SslHelper.parseCertificate(certificateBytes);
Log.i("KDE/Device", "Got certificate ");
} catch (Exception e) {
Log.e("KDE/Device", "Error getting certificate", e);
}
}
Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
if (!pairingHandlers.containsKey(link.getName())) {
BasePairingHandler.PairingHandlerCallback callback = new BasePairingHandler.PairingHandlerCallback() {
@Override
public void incomingRequest() {
for (PairingCallback cb : pairingCallback) {
cb.incomingRequest();
}
}
@Override
public void pairingDone() {
Device.this.pairingDone();
}
@Override
public void pairingFailed(String error) {
for (PairingCallback cb : pairingCallback) {
cb.pairingFailed(error);
}
}
@Override
public void unpaired() {
unpairInternal();
}
};
pairingHandlers.put(link.getName(), link.getPairingHandler(this, callback));
}
Set<String> outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null);
Set<String> incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null);
if (incomingCapabilities != null && outgoingCapabilities != null) {
supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(incomingCapabilities, outgoingCapabilities));
} else {
@@ -504,18 +406,6 @@ public class Device implements BaseLink.PacketReceiver {
public void removeLink(BaseLink link) {
//FilesHelper.LogOpenFileCount();
/* Remove pairing handler corresponding to that link too if it was the only link*/
boolean linkPresent = false;
for (BaseLink bl : links) {
if (bl.getName().equals(link.getName())) {
linkPresent = true;
break;
}
}
if (!linkPresent) {
pairingHandlers.remove(link.getName());
}
link.removePacketReceiver(this);
links.remove(link);
Log.i("KDE/Device", "removeLink: " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
@@ -534,16 +424,8 @@ public class Device implements BaseLink.PacketReceiver {
DeviceStats.countReceived(getDeviceId(), np.getType());
if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) {
Log.i("KDE/Device", "Pair packet");
for (BasePairingHandler ph : pairingHandlers.values()) {
try {
ph.packetReceived(np);
} catch (Exception e) {
Log.e("PairingPacketReceived", "Exception", e);
}
}
pairingHandler.packetReceived(np);
} else if (isPaired()) {
// pluginsByIncomingInterface may not be built yet
if(pluginsByIncomingInterface.isEmpty()) {

View File

@@ -120,4 +120,5 @@ class DevicePacketQueue {
}
}
}
}

View File

@@ -11,14 +11,22 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import com.jaredrummler.android.device.DeviceName;
import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings;
import org.kde.kdeconnect.Device;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
@@ -27,10 +35,13 @@ public class DeviceHelper {
public static final int ProtocolVersion = 7;
public static final String KEY_DEVICE_NAME_PREFERENCE = "device_name_preference";
public static final String KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET = "device_name_downloaded_preference";
public static final String KEY_DEVICE_ID_PREFERENCE = "device_id_preference";
private static boolean fetchingName = false;
public static final String DEVICE_DATABASE = "https://storage.googleapis.com/play_public/supported_devices.csv";
private static boolean isTablet() {
Configuration config = Resources.getSystem().getConfiguration();
//This assumes that the values for the screen sizes are consecutive, so XXLARGE > XLARGE > LARGE
@@ -52,35 +63,55 @@ public class DeviceHelper {
}
}
//It returns getAndroidDeviceName() if no user-defined name has been set with setDeviceName().
public static String getDeviceName(Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
// Could use preferences.contains but would need to check for empty String anyway.
String deviceName = preferences.getString(KEY_DEVICE_NAME_PREFERENCE, "");
if (deviceName.isEmpty()) {
//DeviceName.init(context); // Needed in DeviceName 2.x +
if (!fetchingName) {
fetchingName = true;
DeviceHelper.backgroundFetchDeviceName(context); //Starts a background thread that will eventually update the shared pref
}
return DeviceName.getDeviceName(); //Temp name while we fetch it from the internet
if (!preferences.contains(KEY_DEVICE_NAME_PREFERENCE)
&& !preferences.getBoolean(KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET, false)
&& !fetchingName) {
fetchingName = true;
DeviceHelper.backgroundFetchDeviceName(context);
return Build.MODEL;
}
return deviceName;
return preferences.getString(KEY_DEVICE_NAME_PREFERENCE, Build.MODEL);
}
private static void backgroundFetchDeviceName(final Context context) {
DeviceName.with(context).request((info, error) -> {
ThreadHelper.execute(() -> {
try {
URL url = new URL(DEVICE_DATABASE);
URLConnection connection = url.openConnection();
// If we get here we managed to download the file. Mark that as done so we don't try again even if we don't end up finding a name.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
preferences.edit().putBoolean(KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET, true).apply();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_16))) {
CsvParserSettings settings = new CsvParserSettings();
settings.setHeaderExtractionEnabled(true);
CsvParser parser = new CsvParser(settings);
boolean found = false;
for (String[] records : parser.iterate(reader)) {
if (records.length < 4) {
continue;
}
String buildModel = records[3];
if (Build.MODEL.equals(buildModel)) {
String deviceName = records[1];
Log.i("DeviceHelper", "Got device name: " + deviceName);
// Update the shared preference. Places that display the name should be listening to this change and update it
setDeviceName(context, deviceName);
found = true;
break;
}
}
if (!found) {
Log.e("DeviceHelper", "Didn't find a device name for " + Build.MODEL);
}
}
} catch(IOException e) {
e.printStackTrace();
}
fetchingName = false;
if (error != null) {
Log.e("DeviceHelper", "Error fetching device name");
error.printStackTrace();
}
if (info != null) {
String deviceName = info.getName();
Log.i("DeviceHelper", "Got device name: " + deviceName);
// Update the shared preference. Places that display the name should be listening to this change and update it
setDeviceName(context, deviceName);
}
});
}

View File

@@ -176,26 +176,29 @@ public class SslHelper {
return !cert.isEmpty();
}
private static SSLContext getSslContext(Context context, String deviceId, boolean isDeviceTrusted) {
/**
* Returns the stored certificate for a trusted device
**/
public static Certificate getDeviceCertificate(Context context, String deviceId) throws CertificateException {
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
byte[] certificateBytes = Base64.decode(devicePreferences.getString("certificate", ""), 0);
return parseCertificate(certificateBytes);
}
private static SSLContext getSslContextForDevice(Context context, String deviceId, boolean isDeviceTrusted) {
//TODO: Cache
try {
// Get device private key
PrivateKey privateKey = RsaHelper.getPrivateKey(context);
// Get remote device certificate if trusted
Certificate remoteDeviceCertificate = null;
if (isDeviceTrusted) {
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
byte[] certificateBytes = Base64.decode(devicePreferences.getString("certificate", ""), 0);
remoteDeviceCertificate = parseCertificate(certificateBytes);
}
// Setup keystore
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setKeyEntry("key", privateKey, "".toCharArray(), new Certificate[]{certificate});
// Set certificate if device trusted
if (remoteDeviceCertificate != null) {
// Add device certificate if device trusted
if (isDeviceTrusted) {
Certificate remoteDeviceCertificate = getDeviceCertificate(context, deviceId);
keyStore.setCertificateEntry(deviceId, remoteDeviceCertificate);
}
@@ -239,7 +242,7 @@ public class SslHelper {
}
public static SSLSocket convertToSslSocket(Context context, Socket socket, String deviceId, boolean isDeviceTrusted, boolean clientMode) throws IOException {
SSLSocketFactory sslsocketFactory = SslHelper.getSslContext(context, deviceId, isDeviceTrusted).getSocketFactory();
SSLSocketFactory sslsocketFactory = SslHelper.getSslContextForDevice(context, deviceId, isDeviceTrusted).getSocketFactory();
SSLSocket sslsocket = (SSLSocket) sslsocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
SslHelper.configureSslSocket(sslsocket, isDeviceTrusted, clientMode);
return sslsocket;

View File

@@ -5,6 +5,8 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Helpers.DeviceHelper;
@@ -14,8 +16,11 @@ import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.PairingHandler;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -104,16 +109,21 @@ public class KdeConnect extends Application {
for (String deviceId : trustedDevices) {
//Log.e("BackgroundService", "Loading device "+deviceId);
if (preferences.getBoolean(deviceId, false)) {
Device device = new Device(this, deviceId);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
try {
Device device = new Device(this, deviceId);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
} catch (CertificateException e) {
Log.e("KdeConnect", "Could not load trusted device, certificate not valid: " + deviceId);
e.printStackTrace();
}
}
}
}
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
private final PairingHandler.PairingCallback devicePairingCallback = new PairingHandler.PairingCallback() {
@Override
public void incomingRequest() {
public void incomingPairRequest() {
onDeviceListChanged();
}
@@ -135,15 +145,17 @@ public class KdeConnect extends Application {
private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) {
String deviceId = identityPacket.getString("deviceId");
public void onConnectionReceived(@NonNull final String deviceId,
@NonNull final Certificate certificate,
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link) {
Device device = devices.get(deviceId);
if (device != null) {
Log.i("KDE/Application", "addLink, known device: " + deviceId);
device.addLink(identityPacket, link);
} else {
Log.i("KDE/Application", "addLink,unknown device: " + deviceId);
device = new Device(KdeConnect.this, identityPacket, link);
device = new Device(KdeConnect.this, deviceId, certificate, identityPacket, link);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}

View File

@@ -86,7 +86,7 @@ internal fun updateAppWidget(
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
Log.i("WidgetProvider", "updateAppWidget: $appWidgetId")
Log.d("WidgetProvider", "updateAppWidget: $appWidgetId")
val deviceId = loadWidgetDeviceIdPref(context, appWidgetId)
val device: Device? = if (deviceId != null) KdeConnect.getInstance().getDevice(deviceId) else null
@@ -100,6 +100,8 @@ internal fun updateAppWidget(
val setDevicePendingIntent = PendingIntent.getActivity(context, appWidgetId, setDeviceIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.runcommandWidgetTitleHeader, setDevicePendingIntent)
Log.d("WidgetProvider", "updateAppWidget device: " + if (device == null) "null" else device.name)
if (device == null) {
views.setTextViewText(R.id.runcommandWidgetTitle, context.getString(R.string.kde_connect))
views.setViewVisibility(R.id.run_commands_list, View.VISIBLE)

View File

@@ -31,7 +31,6 @@ import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device
import org.kde.kdeconnect.Device.PairingCallback
import org.kde.kdeconnect.Device.PluginsChangedListener
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect.KdeConnect
@@ -300,8 +299,8 @@ class DeviceFragment : Fragment() {
}
}
private val pairingCallback: PairingCallback = object : PairingCallback {
override fun incomingRequest() {
private val pairingCallback: PairingHandler.PairingCallback = object : PairingHandler.PairingCallback {
override fun incomingPairRequest() {
mActivity?.runOnUiThread { refreshUI() }
}

View File

@@ -0,0 +1,205 @@
/*
* SPDX-FileCopyrightText: 2023 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.UserInterface;
import android.util.Log;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect_tp.R;
import java.util.Timer;
import java.util.TimerTask;
public class PairingHandler {
public enum PairState {
NotPaired,
Requested,
RequestedByPeer,
Paired
}
public interface PairingCallback {
void incomingPairRequest();
void pairingFailed(String error);
void pairingSuccessful();
void unpaired();
}
protected final Device mDevice;
protected PairState mPairState;
protected final PairingCallback mCallback;
public PairState getState() {
return mPairState;
}
private Timer mPairingTimer;
public PairingHandler(Device device, final PairingCallback callback, PairState initialState) {
this.mDevice = device;
this.mCallback = callback;
this.mPairState = initialState;
}
public void packetReceived(NetworkPacket np) {
cancelTimer();
boolean wantsPair = np.getBoolean("pair");
if (wantsPair) {
switch (mPairState) {
case Requested: // We started pairing and tis is a confirmation
pairingDone();
break;
case RequestedByPeer:
Log.w("PairingHandler", "Ignoring second pairing request before the first one timed out");
break;
case Paired:
Log.w("PairingHandler", "Auto-accepting pairing request from a device we already trusted");
acceptPairing();
break;
case NotPaired:
mPairState = PairState.RequestedByPeer;
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.w("PairingHandler", "Unpairing (timeout after we started pairing)");
mPairState = PairState.NotPaired;
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out));
}
}, 25 * 1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds)
mCallback.incomingPairRequest();
break;
}
} else {
Log.i("PairingHandler", "Unpair request received");
switch (mPairState) {
case NotPaired:
Log.i("PairingHandler", "Ignoring unpair request for already unpaired device");
break;
case Requested: // We started pairing and got rejected
case RequestedByPeer: // They stared pairing, then cancelled
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
break;
case Paired:
mCallback.unpaired();
break;
}
mPairState = PairState.NotPaired;
}
}
public void requestPairing() {
cancelTimer();
if (mPairState == PairState.Paired) {
Log.w("PairingHandler", "requestPairing was called on an already paired device");
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_already_paired));
return;
}
if (mPairState == PairState.RequestedByPeer) {
Log.w("PairingHandler", "Pairing already started by the other end, accepting their request.");
acceptPairing();
return;
}
if (!mDevice.isReachable()) {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
return;
}
mPairState = PairState.Requested;
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.w("PairingHandler","Unpairing (timeout after receiving pair request)");
mPairState = PairState.NotPaired;
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out));
}
}, 30*1000); //Time to wait for the other to accept
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() { }
@Override
public void onFailure(Throwable e) {
cancelTimer();
Log.e("PairingHandler", "Exception sending pairing request", e);
mPairState = PairState.NotPaired;
mCallback.pairingFailed(mDevice.getContext().getString(R.string.runcommand_notreachable));
}
};
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", true);
mDevice.sendPacket(np, statusCallback);
}
public void acceptPairing() {
cancelTimer();
Device.SendPacketStatusCallback StateCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
pairingDone();
}
@Override
public void onFailure(Throwable e) {
Log.e("PairingHandler", "Exception sending accept pairing packet", e);
mPairState = PairState.NotPaired;
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
}
};
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", true);
mDevice.sendPacket(np, StateCallback);
}
public void cancelPairing() {
cancelTimer();
mPairState = PairState.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_user));
}
private void pairingDone() {
Log.i("PairingHandler", "Pairing done");
mPairState = PairState.Paired;
try {
mCallback.pairingSuccessful();
} catch (Exception e) {
Log.e("PairingHandler", "Exception in pairingSuccessful callback, unpairing");
e.printStackTrace();
mPairState = PairState.NotPaired;
}
}
public void unpair() {
mPairState = PairState.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
mCallback.unpaired();
}
private void cancelTimer() {
if (mPairingTimer != null) {
mPairingTimer.cancel();
}
}
}

View File

@@ -28,21 +28,24 @@ import androidx.core.content.ContextCompat;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Backends.LanBackend.LanLink;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanPairingHandler;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.mockito.ArgumentCaptor;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.UserInterface.PairingHandler;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Base64.class, Log.class, PreferenceManager.class, ContextCompat.class})
@@ -57,16 +60,22 @@ public class DeviceTest {
String deviceId = "testDevice";
String name = "Test Device";
KeyPair keyPair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch (Exception e) {
Log.e("KDE/initializeRsaKeys", "Exception", e);
return;
}
String encodedCertificate = "MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
"MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk\n" +
"ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH\n" +
"ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s\n" +
"WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2\n" +
"qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5\n" +
"KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg\n" +
"7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD\n" +
"VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM\n" +
"wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn\n" +
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
"7n+KOQ==";
this.context = Mockito.mock(Context.class);
@@ -81,7 +90,7 @@ public class DeviceTest {
SharedPreferences.Editor editor = deviceSettings.edit();
editor.putString("deviceName", name);
editor.putString("deviceType", Device.DeviceType.Phone.toString());
editor.putString("publicKey", Base64.encodeToString(keyPair.getPublic().getEncoded(), 0).trim() + "\n");
editor.putString("certificate", encodedCertificate);
editor.apply();
Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings);
@@ -116,71 +125,25 @@ public class DeviceTest {
// Basic paired device testing
@Test
public void testDevice() {
public void testDevice() throws CertificateException {
Device device = new Device(context, "testDevice");
assertEquals(device.getDeviceId(), "testDevice");
assertEquals(device.getDeviceType(), Device.DeviceType.Phone);
assertEquals(device.getName(), "Test Device");
assertTrue(device.isPaired());
assertNotNull(device.certificate);
}
// Testing pairing done
// Created an unpaired device inside this test
@Test
public void testPairingDone() {
public void testPairingDone() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, CertificateException {
NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
fakeNetworkPacket.set("deviceId", "unpairedTestDevice");
String deviceId = "unpairedTestDevice";
fakeNetworkPacket.set("deviceId", deviceId);
fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
fakeNetworkPacket.set("deviceType", Device.DeviceType.Phone.toString());
LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class);
Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider");
LanLink link = Mockito.mock(LanLink.class);
Mockito.when(link.getLinkProvider()).thenReturn(linkProvider);
Mockito.when(link.getPairingHandler(any(Device.class), any(BasePairingHandler.PairingHandlerCallback.class))).thenReturn(Mockito.mock(LanPairingHandler.class));
Device device = new Device(context, fakeNetworkPacket, link);
Device.PairingCallback pairingCallback = Mockito.mock(Device.PairingCallback.class);
device.addPairingCallback(pairingCallback);
ArgumentCaptor<BasePairingHandler.PairingHandlerCallback> pairingHandlerCallback = ArgumentCaptor.forClass(BasePairingHandler.PairingHandlerCallback.class);
Mockito.verify(link, Mockito.times(1)).getPairingHandler(eq(device), pairingHandlerCallback.capture());
assertNotNull(device);
assertEquals(device.getDeviceId(), "unpairedTestDevice");
assertEquals(device.getName(), "Unpaired Test Device");
assertEquals(device.getDeviceType(), Device.DeviceType.Phone);
assertNull(device.certificate);
pairingHandlerCallback.getValue().pairingDone();
assertTrue(device.isPaired());
Mockito.verify(pairingCallback, Mockito.times(1)).pairingSuccessful();
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
assertTrue(preferences.getBoolean(device.getDeviceId(), false));
SharedPreferences settings = context.getSharedPreferences(device.getDeviceId(), Context.MODE_PRIVATE);
assertEquals(settings.getString("deviceName", "Unknown device"), "Unpaired Test Device");
assertEquals(settings.getString("deviceType", "tablet"), "phone");
// Cleanup for unpaired test device
preferences.edit().remove(device.getDeviceId()).apply();
settings.edit().clear().apply();
}
@Test
public void testPairingDoneWithCertificate() {
NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
fakeNetworkPacket.set("deviceId", "unpairedTestDevice");
fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
fakeNetworkPacket.set("deviceType", Device.DeviceType.Phone.toString());
fakeNetworkPacket.set("certificate",
String certificateString =
"MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
@@ -196,29 +159,25 @@ public class DeviceTest {
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
"7n+KOQ==");
"7n+KOQ==";
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class);
Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider");
LanLink link = Mockito.mock(LanLink.class);
Mockito.when(link.getPairingHandler(any(Device.class), any(BasePairingHandler.PairingHandlerCallback.class))).thenReturn(Mockito.mock(LanPairingHandler.class));
Mockito.when(link.getLinkProvider()).thenReturn(linkProvider);
Device device = new Device(context, fakeNetworkPacket, link);
Device device = new Device(context, deviceId, certificate, fakeNetworkPacket, link);
assertNotNull(device);
assertEquals(device.getDeviceId(), "unpairedTestDevice");
assertEquals(device.getDeviceId(), deviceId);
assertEquals(device.getName(), "Unpaired Test Device");
assertEquals(device.getDeviceType(), Device.DeviceType.Phone);
assertNotNull(device.certificate);
Method method;
try {
method = Device.class.getDeclaredMethod("pairingDone");
method.setAccessible(true);
method.invoke(device);
} catch (Exception e) {
Log.e("KDEConnect", "Exception", e);
}
Method method = PairingHandler.class.getDeclaredMethod("pairingDone");
method.setAccessible(true);
method.invoke(device.pairingHandler);
assertTrue(device.isPaired());
@@ -235,8 +194,8 @@ public class DeviceTest {
}
@Test
public void testUnpair() {
Device.PairingCallback pairingCallback = Mockito.mock(Device.PairingCallback.class);
public void testUnpair() throws CertificateException {
PairingHandler.PairingCallback pairingCallback = Mockito.mock(PairingHandler.PairingCallback.class);
Device device = new Device(context, "testDevice");
device.addPairingCallback(pairingCallback);