diff --git a/src/org/kde/kdeconnect/Backends/BaseLink.java b/src/org/kde/kdeconnect/Backends/BaseLink.java index 18c468db..a238a389 100644 --- a/src/org/kde/kdeconnect/Backends/BaseLink.java +++ b/src/org/kde/kdeconnect/Backends/BaseLink.java @@ -1,95 +1,92 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Backends; import android.content.Context; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import java.security.PrivateKey; -import java.security.PublicKey; import java.util.ArrayList; public abstract class BaseLink { protected final Context context; public interface PacketReceiver { void onPacketReceived(NetworkPacket np); } private final BaseLinkProvider linkProvider; private final String deviceId; private final ArrayList receivers = new ArrayList<>(); protected PrivateKey privateKey; protected BaseLink(Context context, String deviceId, BaseLinkProvider linkProvider) { 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(Device device, BasePairingHandler.PairingHandlerCallback callback); public String getDeviceId() { return deviceId; } public void setPrivateKey(PrivateKey key) { privateKey = key; } public BaseLinkProvider getLinkProvider() { return linkProvider; } //The daemon will periodically destroy unpaired links if this returns false public boolean linkShouldBeKeptAlive() { return false; } public void addPacketReceiver(PacketReceiver pr) { receivers.add(pr); } public void removePacketReceiver(PacketReceiver pr) { receivers.remove(pr); } //Should be called from a background thread listening to packages protected void packageReceived(NetworkPacket np) { for(PacketReceiver pr : receivers) { pr.onPacketReceived(np); } } public void disconnect() { linkProvider.connectionLost(this); } //TO OVERRIDE, should be sync public abstract boolean sendPacket(NetworkPacket np, Device.SendPacketStatusCallback callback); - @Deprecated - public abstract boolean sendPacketEncrypted(NetworkPacket np, Device.SendPacketStatusCallback callback, PublicKey key); } diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java index 4a857130..33f972f9 100644 --- a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLink.java @@ -1,250 +1,222 @@ /* * Copyright 2016 Saikrishna Arcot * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Backends.BluetoothBackend; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Build; import android.util.Log; 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.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.NetworkPacket; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.nio.charset.Charset; -import java.security.PublicKey; import java.util.UUID; public class BluetoothLink extends BaseLink { private final BluetoothSocket socket; private final BluetoothLinkProvider linkProvider; private boolean continueAccepting = true; private final Thread receivingThread = new Thread(new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); try { Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8"); char[] buf = new char[512]; while (continueAccepting) { while (sb.indexOf("\n") == -1 && continueAccepting) { int charsRead; if ((charsRead = reader.read(buf)) > 0) { sb.append(buf, 0, charsRead); } } int endIndex = sb.indexOf("\n"); if (endIndex != -1) { String message = sb.substring(0, endIndex + 1); sb.delete(0, endIndex + 1); processMessage(message); } } } catch (IOException e) { Log.e("BluetoothLink/receiving", "Connection to " + socket.getRemoteDevice().getAddress() + " likely broken.", e); disconnect(); } } private void processMessage(String message) { NetworkPacket np; try { np = NetworkPacket.unserialize(message); } catch (JSONException e) { Log.e("BluetoothLink/receiving", "Unable to parse message.", e); return; } - if (np.getType().equals(NetworkPacket.PACKET_TYPE_ENCRYPTED)) { - try { - np = RsaHelper.decrypt(np, privateKey); - } catch (Exception e) { - Log.e("BluetoothLink/receiving", "Exception decrypting the package", e); - } - } - if (np.hasPayloadTransferInfo()) { BluetoothSocket transferSocket = null; try { UUID transferUuid = UUID.fromString(np.getPayloadTransferInfo().getString("uuid")); transferSocket = socket.getRemoteDevice().createRfcommSocketToServiceRecord(transferUuid); transferSocket.connect(); np.setPayload(new NetworkPacket.Payload(transferSocket.getInputStream(), np.getPayloadSize())); } catch (Exception e) { if (transferSocket != null) { try { transferSocket.close(); } catch (IOException ignored) { } } Log.e("BluetoothLink/receiving", "Unable to get payload", e); } } packageReceived(np); } }); public BluetoothLink(Context context, BluetoothSocket socket, String deviceId, BluetoothLinkProvider linkProvider) { super(context, deviceId, linkProvider); this.socket = socket; this.linkProvider = linkProvider; } public void startListening() { this.receivingThread.start(); } @Override public String getName() { return "BluetoothLink"; } @Override public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { return new BluetoothPairingHandler(device, callback); } public void disconnect() { if (socket == null) { return; } continueAccepting = false; try { socket.close(); } catch (IOException ignored) { } linkProvider.disconnectedLink(this, getDeviceId(), socket); } private void sendMessage(NetworkPacket np) throws JSONException, IOException { byte[] message = np.serialize().getBytes(Charset.forName("UTF-8")); OutputStream socket = this.socket.getOutputStream(); Log.i("BluetoothLink", "Beginning to send message"); socket.write(message); Log.i("BluetoothLink", "Finished sending message"); } @Override - public boolean sendPacket(NetworkPacket np, Device.SendPacketStatusCallback callback) { - return sendPacketInternal(np, callback, null); - } - - @Override - public boolean sendPacketEncrypted(NetworkPacket np, Device.SendPacketStatusCallback callback, PublicKey key) { - return sendPacketInternal(np, callback, key); - } - - private boolean sendPacketInternal(NetworkPacket np, final Device.SendPacketStatusCallback callback, PublicKey key) { + public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) { /*if (!isConnected()) { Log.e("BluetoothLink", "sendPacketEncrypted failed: not connected"); callback.sendFailure(new Exception("Not connected")); return; }*/ try { BluetoothServerSocket serverSocket = null; if (np.hasPayload()) { UUID transferUuid = UUID.randomUUID(); serverSocket = BluetoothAdapter.getDefaultAdapter() .listenUsingRfcommWithServiceRecord("KDE Connect Transfer", transferUuid); JSONObject payloadTransferInfo = new JSONObject(); payloadTransferInfo.put("uuid", transferUuid.toString()); np.setPayloadTransferInfo(payloadTransferInfo); } - if (key != null) { - try { - np = RsaHelper.encrypt(np, key); - } catch (Exception e) { - callback.onFailure(e); - return false; - } - } - sendMessage(np); if (serverSocket != null) { try (BluetoothSocket transferSocket = serverSocket.accept()) { serverSocket.close(); int idealBufferLength = 4096; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && transferSocket.getMaxReceivePacketSize() > 0) { idealBufferLength = transferSocket.getMaxReceivePacketSize(); } byte[] buffer = new byte[idealBufferLength]; int bytesRead; long progress = 0; InputStream stream = np.getPayload().getInputStream(); while ((bytesRead = stream.read(buffer)) != -1) { progress += bytesRead; transferSocket.getOutputStream().write(buffer, 0, bytesRead); if (np.getPayloadSize() > 0) { callback.onProgressChanged((int) (100 * progress / np.getPayloadSize())); } } transferSocket.getOutputStream().flush(); stream.close(); } catch (Exception e) { callback.onFailure(e); return false; } } callback.onSuccess(); return true; } catch (Exception e) { callback.onFailure(e); return false; } } @Override public boolean linkShouldBeKeptAlive() { return receivingThread.isAlive(); } /* public boolean isConnected() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { return socket.isConnected(); } else { return true; } } */ } diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java index d5743357..d86de46e 100644 --- a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java @@ -1,195 +1,195 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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 packageReceived(NetworkPacket np) throws Exception { + public void packageReceived(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.error_could_not_send_package)); } }; 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 rejectPairing() { 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); } } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java index 0a5485ec..f4847fa0 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java @@ -1,297 +1,265 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Backends.LanBackend; import android.content.Context; import android.util.Log; 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.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.channels.NotYetConnectedException; -import java.security.PublicKey; import javax.net.ssl.SSLSocket; public class LanLink extends BaseLink { public interface LinkDisconnectedCallback { void linkDisconnected(LanLink brokenLink); } public enum ConnectionStarted { Locally, Remotely } private ConnectionStarted connectionSource; // If the other device sent me a broadcast, // I should not close the connection with it // because it's probably trying to find me and // potentially ask for pairing. - private volatile Socket socket = null; + private volatile SSLSocket socket = null; private final LinkDisconnectedCallback callback; @Override public void disconnect() { Log.i("LanLink/Disconnect","socket:"+ socket.hashCode()); try { socket.close(); } catch (IOException e) { Log.e("LanLink", "Error", e); } } //Returns the old socket - public Socket reset(final Socket newSocket, ConnectionStarted connectionSource) throws IOException { + public SSLSocket reset(final SSLSocket newSocket, ConnectionStarted connectionSource) throws IOException { - Socket oldSocket = socket; + SSLSocket oldSocket = socket; socket = newSocket; this.connectionSource = connectionSource; if (oldSocket != null) { oldSocket.close(); //This should cancel the readThread } //Log.e("LanLink", "Start listening"); //Create a thread to take care of incoming data for the new socket new Thread(() -> { try { BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8)); while (true) { String packet; try { packet = reader.readLine(); } catch (SocketTimeoutException e) { continue; } if (packet == null) { throw new IOException("End of stream"); } if (packet.isEmpty()) { continue; } NetworkPacket np = NetworkPacket.unserialize(packet); receivedNetworkPacket(np); } } catch (Exception e) { Log.i("LanLink", "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage()); try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile boolean thereIsaANewSocket = (newSocket != socket); if (!thereIsaANewSocket) { callback.linkDisconnected(LanLink.this); } } }).start(); return oldSocket; } - public LanLink(Context context, String deviceId, LanLinkProvider linkProvider, Socket socket, ConnectionStarted connectionSource) throws IOException { + public LanLink(Context context, String deviceId, LanLinkProvider linkProvider, SSLSocket socket, ConnectionStarted connectionSource) throws IOException { super(context, deviceId, linkProvider); callback = linkProvider; reset(socket, connectionSource); } @Override public String getName() { return "LanLink"; } @Override public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { return new LanPairingHandler(device, callback); } //Blocking, do not call from main thread - private boolean sendPacketInternal(NetworkPacket np, final Device.SendPacketStatusCallback callback, PublicKey key) { + @Override + public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) { if (socket == null) { Log.e("KDE/sendPacket", "Not yet connected"); callback.onFailure(new NotYetConnectedException()); return false; } try { //Prepare socket for the payload final ServerSocket server; if (np.hasPayload()) { server = LanLinkProvider.openServerSocketOnFreePort(LanLinkProvider.PAYLOAD_TRANSFER_MIN_PORT); JSONObject payloadTransferInfo = new JSONObject(); payloadTransferInfo.put("port", server.getLocalPort()); np.setPayloadTransferInfo(payloadTransferInfo); } else { server = null; } - //Encrypt if key provided - if (key != null) { - np = RsaHelper.encrypt(np, key); - } - //Log.e("LanLink/sendPacket", np.getType()); //Send body of the network package try { OutputStream writer = socket.getOutputStream(); writer.write(np.serialize().getBytes(StringsHelper.UTF8)); writer.flush(); } catch (Exception e) { disconnect(); //main socket is broken, disconnect throw e; } //Send payload if (server != null) { Socket payloadSocket = null; OutputStream outputStream = null; InputStream inputStream; try { //Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards server.setSoTimeout(10*1000); payloadSocket = server.accept(); //Convert to SSL if needed - if (socket instanceof SSLSocket) { - payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); - } + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false); outputStream = payloadSocket.getOutputStream(); inputStream = np.getPayload().getInputStream(); Log.i("KDE/LanLink", "Beginning to send payload"); byte[] buffer = new byte[4096]; int bytesRead; long size = np.getPayloadSize(); long progress = 0; long timeSinceLastUpdate = -1; while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) { //Log.e("ok",""+bytesRead); progress += bytesRead; outputStream.write(buffer, 0, bytesRead); if (size > 0) { if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second long percent = ((100 * progress) / size); callback.onProgressChanged((int) percent); timeSinceLastUpdate = System.currentTimeMillis(); } } } outputStream.flush(); Log.i("KDE/LanLink", "Finished sending payload ("+progress+" bytes written)"); } finally { try { server.close(); } catch (Exception ignored) { } try { payloadSocket.close(); } catch (Exception ignored) { } np.getPayload().close(); try { outputStream.close(); } catch (Exception ignored) { } } } if (!np.isCanceled()) { callback.onSuccess(); } return true; } catch (Exception e) { if (callback != null) { callback.onFailure(e); } return false; } finally { //Make sure we close the payload stream, if any if (np.hasPayload()) { np.getPayload().close(); } } } - - //Blocking, do not call from main thread - @Override - public boolean sendPacket(NetworkPacket np, Device.SendPacketStatusCallback callback) { - return sendPacketInternal(np, callback, null); - } - - //Blocking, do not call from main thread - @Override - public boolean sendPacketEncrypted(NetworkPacket np, Device.SendPacketStatusCallback callback, PublicKey key) { - return sendPacketInternal(np, callback, key); - } - private void receivedNetworkPacket(NetworkPacket np) { - if (np.getType().equals(NetworkPacket.PACKET_TYPE_ENCRYPTED)) { - try { - np = RsaHelper.decrypt(np, privateKey); - } catch(Exception e) { - Log.e("KDE/onPacketReceived","Exception decrypting the package", e); - } - } - if (np.hasPayloadTransferInfo()) { Socket payloadSocket = new Socket(); try { int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress deviceAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); payloadSocket.connect(new InetSocketAddress(deviceAddress.getAddress(), tcpPort)); - // Use ssl if existing link is on ssl - if (socket instanceof SSLSocket) { - payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); - } + payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true); np.setPayload(new NetworkPacket.Payload(payloadSocket, np.getPayloadSize())); } catch (Exception e) { try { payloadSocket.close(); } catch(Exception ignored) { } Log.e("KDE/LanLink", "Exception connecting to payload remote socket", e); } } packageReceived(np); } @Override public boolean linkShouldBeKeptAlive() { return true; //FIXME: Current implementation is broken, so for now we will keep links always established //We keep the remotely initiated connections, since the remotes require them if they want to request //pairing to us, or connections that are already paired. //return (connectionSource == ConnectionStarted.Remotely); } } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java index 74a93555..b2d539fd 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java @@ -1,457 +1,444 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.StringsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.CustomDevicesActivity; 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.security.cert.Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; import javax.net.SocketFactory; import javax.net.ssl.SSLSocket; /** * This BaseLinkProvider creates {@link LanLink}s to other devices on the same * WiFi network. The first packet sent over a socket must be an * {@link NetworkPacket#createIdentityPacket(Context)}. * * @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted) */ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDisconnectedCallback { - public static final int MIN_VERSION_WITH_SSL_SUPPORT = 6; - private static final int MIN_VERSION_WITH_NEW_PORT_SUPPORT = 7; - private final static int MIN_PORT = 1716; private final static int MAX_PORT = 1764; final static int PAYLOAD_TRANSFER_MIN_PORT = 1739; private final Context context; private final HashMap visibleComputers = new HashMap<>(); //Links by device id private ServerSocket tcpServer; private DatagramSocket udpServer; private boolean listening = false; // To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP private final ArrayList reverseConnectionBlackList = new ArrayList<>(); @Override // SocketClosedCallback public void linkDisconnected(LanLink brokenLink) { String deviceId = brokenLink.getDeviceId(); visibleComputers.remove(deviceId); connectionLost(brokenLink); } //They received my UDP broadcast and are connecting to me. The first thing they sned should be their identity. - private void tcpPacketReceived(Socket socket) throws Exception { + private void tcpPacketReceived(Socket socket) { NetworkPacket networkPacket; try { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String message = reader.readLine(); networkPacket = NetworkPacket.unserialize(message); //Log.e("TcpListener","Received TCP package: "+networkPacket.serialize()); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Exception while receiving TCP packet", e); return; } if (!networkPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { Log.e("KDE/LanLinkProvider", "Expecting an identity package instead of " + networkPacket.getType()); return; } Log.i("KDE/LanLinkProvider", "Identity package received from a TCP connection from " + networkPacket.getString("deviceName")); identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally); } //I've received their broadcast and should connect to their TCP socket and send my identity. - private void udpPacketReceived(DatagramPacket packet) throws Exception { + private void udpPacketReceived(DatagramPacket packet) { final InetAddress address = packet.getAddress(); try { String message = new String(packet.getData(), StringsHelper.UTF8); final NetworkPacket identityPacket = NetworkPacket.unserialize(message); final String deviceId = identityPacket.getString("deviceId"); if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) { Log.e("KDE/LanLinkProvider", "Expecting an UDP identity package"); return; } else { String myId = DeviceHelper.getDeviceId(context); if (deviceId.equals(myId)) { //Ignore my own broadcast return; } } - if (identityPacket.getInt("protocolVersion") >= MIN_VERSION_WITH_NEW_PORT_SUPPORT && identityPacket.getInt("tcpPort") < MIN_PORT) { - Log.w("KDE/LanLinkProvider", "Ignoring a udp broadcast from legacy port because it comes from a device which knows about the new port."); - return; - } - Log.i("KDE/LanLinkProvider", "Broadcast identity package received from " + identityPacket.getString("deviceName")); int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT); SocketFactory socketFactory = SocketFactory.getDefault(); Socket socket = socketFactory.createSocket(address, tcpPort); configureSocket(socket); OutputStream out = socket.getOutputStream(); NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context); out.write(myIdentity.serialize().getBytes()); out.flush(); identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Cannot connect to " + address, e); if (!reverseConnectionBlackList.contains(address)) { Log.w("KDE/LanLinkProvider", "Blacklisting " + address); reverseConnectionBlackList.add(address); new Timer().schedule(new TimerTask() { @Override public void run() { reverseConnectionBlackList.remove(address); } }, 5 * 1000); // Try to cause a reverse connection onNetworkChange(); } } } private void configureSocket(Socket socket) { try { socket.setKeepAlive(true); } catch (SocketException e) { Log.e("LanLink", "Exception", e); } } /** * Called when a new 'identity' packet is received. Those are passed here by * {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}. *

* If the remote device should be connected, this calls {@link #addLink}. * Otherwise, if there was an Exception, we unpair from that device. *

* * @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 */ private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) { 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); - // Add ssl handler if device uses new protocol + // Do the SSL handshake try { - if (identityPacket.getInt("protocolVersion") >= MIN_VERSION_WITH_SSL_SUPPORT) { + SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); + boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); + + if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) { + //Device paired with and old version, we can't use it as we lack the certificate + BackgroundService.RunCommand(context, service -> { + Device device = service.getDevice(deviceId); + if (device == null) return; + device.unpair(); + //Retry as unpaired + identityPacketReceived(identityPacket, socket, connectionStarted); + }); + } - SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); - boolean isDeviceTrusted = preferences.getBoolean(deviceId, false); + Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); - if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) { - //Device paired with and old version, we can't use it as we lack the certificate + final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); + sslsocket.addHandshakeCompletedListener(event -> { + 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, connectionStarted); + } catch (Exception e) { + Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e); BackgroundService.RunCommand(context, service -> { Device device = service.getDevice(deviceId); if (device == null) return; device.unpair(); - //Retry as unpaired - identityPacketReceived(identityPacket, socket, connectionStarted); }); } - - Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); - - final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); - sslsocket.addHandshakeCompletedListener(event -> { - 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, connectionStarted); - } catch (Exception e) { - Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e); - BackgroundService.RunCommand(context, service -> { - Device device = service.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 + new Thread(() -> { + try { + synchronized (this) { + sslsocket.startHandshake(); } - }); - //Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection - new Thread(() -> { - try { - synchronized (this) { - sslsocket.startHandshake(); - } - } catch (Exception e) { - Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName"), e); + } catch (Exception e) { + Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName"), e); - //String[] ciphers = sslsocket.getSupportedCipherSuites(); - //for (String cipher : ciphers) { - // Log.i("SupportedCiphers","cipher: " + cipher); - //} - } - }).start(); - } else { - addLink(identityPacket, socket, connectionStarted); - } + //String[] ciphers = sslsocket.getSupportedCipherSuites(); + //for (String cipher : ciphers) { + // Log.i("SupportedCiphers","cipher: " + cipher); + //} + } + }).start(); } catch (Exception e) { Log.e("LanLink", "Exception", e); } } /** * Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one * link is operated on at a time. *

* Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in * {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27). *

* * @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 * @throws IOException if an exception is thrown by {@link LanLink#reset(Socket, LanLink.ConnectionStarted)} */ - private void addLink(final NetworkPacket identityPacket, Socket socket, LanLink.ConnectionStarted connectionOrigin) throws IOException { + private void addLink(final NetworkPacket identityPacket, SSLSocket socket, LanLink.ConnectionStarted connectionOrigin) throws IOException { String deviceId = identityPacket.getString("deviceId"); LanLink currentLink = visibleComputers.get(deviceId); if (currentLink != null) { //Update old link Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId); final Socket oldSocket = currentLink.reset(socket, connectionOrigin); //Log.e("KDE/LanLinkProvider", "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode()); } else { Log.i("KDE/LanLinkProvider", "Creating a new link for device " + deviceId); //Let's create the link LanLink link = new LanLink(context, deviceId, this, socket, connectionOrigin); visibleComputers.put(deviceId, link); connectionAccepted(identityPacket, link); } } public LanLinkProvider(Context context) { this.context = context; } private void setupUdpListener() { try { udpServer = new DatagramSocket(MIN_PORT); udpServer.setReuseAddress(true); udpServer.setBroadcast(true); } catch (SocketException e) { Log.e("LanLinkProvider", "Error creating udp server", e); return; } new Thread(() -> { while (listening) { final int bufferSize = 1024 * 512; byte[] data = new byte[bufferSize]; DatagramPacket packet = new DatagramPacket(data, bufferSize); try { udpServer.receive(packet); udpPacketReceived(packet); } catch (Exception e) { Log.e("LanLinkProvider", "UdpReceive exception", e); } } Log.w("UdpListener", "Stopping UDP listener"); }).start(); } private void setupTcpListener() { try { tcpServer = openServerSocketOnFreePort(MIN_PORT); } catch (Exception e) { Log.e("LanLinkProvider", "Error creating tcp server", e); return; } new Thread(() -> { while (listening) { try { Socket socket = tcpServer.accept(); configureSocket(socket); tcpPacketReceived(socket); } catch (Exception e) { Log.e("LanLinkProvider", "TcpReceive exception", e); } } Log.w("TcpListener", "Stopping TCP listener"); }).start(); } 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 broadcastUdpPacket() { if (NetworkHelper.isOnMobileNetwork(context)) { Log.w("LanLinkProvider", "On 3G network, not sending broadcast."); return; } new Thread(() -> { ArrayList iplist = CustomDevicesActivity .getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context)); iplist.add("255.255.255.255"); //Default: broadcast. NetworkPacket identity = NetworkPacket.createIdentityPacket(context); int port = (tcpServer == null || !tcpServer.isBound()) ? MIN_PORT : tcpServer.getLocalPort(); identity.set("tcpPort", port); DatagramSocket socket = null; byte[] bytes = null; try { socket = new DatagramSocket(); socket.setReuseAddress(true); socket.setBroadcast(true); bytes = identity.serialize().getBytes(StringsHelper.UTF8); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e); } if (bytes != null) { //Log.e("KDE/LanLinkProvider","Sending packet to "+iplist.size()+" ips"); for (String ipstr : iplist) { try { InetAddress client = InetAddress.getByName(ipstr); socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT)); //Log.i("KDE/LanLinkProvider","Udp identity package sent to address "+client); } catch (Exception e) { Log.e("KDE/LanLinkProvider", "Sending udp identity package failed. Invalid address? (" + ipstr + ")", e); } } } if (socket != null) { socket.close(); } }).start(); } @Override public void onStart() { //Log.i("KDE/LanLinkProvider", "onStart"); if (!listening) { listening = true; setupUdpListener(); setupTcpListener(); broadcastUdpPacket(); } } @Override public void onNetworkChange() { broadcastUdpPacket(); } @Override public void onStop() { //Log.i("KDE/LanLinkProvider", "onStop"); listening = false; 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"; } } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanPairingHandler.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanPairingHandler.java index 049efa93..4a6906af 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanPairingHandler.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanPairingHandler.java @@ -1,240 +1,216 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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.BasePairingHandler; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect_tp.R; -import java.security.KeyFactory; import java.security.cert.CertificateEncodingException; -import java.security.spec.X509EncodedKeySpec; 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); - SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(mDevice.getContext()); - String publicKey = "-----BEGIN PUBLIC KEY-----\n" + globalSettings.getString("publicKey", "").trim()+ "\n-----END PUBLIC KEY-----\n"; - np.set("publicKey", publicKey); return np; } @Override - public void packageReceived(NetworkPacket np) throws Exception{ + public void packageReceived(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) { - //Retrieve their public key - try { - String publicKeyContent = np.getString("publicKey").replace("-----BEGIN PUBLIC KEY-----\n","").replace("-----END PUBLIC KEY-----\n", ""); - byte[] publicKeyBytes = Base64.decode(publicKeyContent, 0); - mDevice.publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes)); - } catch (Exception e) { - //IGNORE - } - 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.error_could_not_send_package)); } }; 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 rejectPairing() { 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(); - if (mDevice.publicKey != null) { - try { - String encodedPublicKey = Base64.encodeToString(mDevice.publicKey.getEncoded(), 0); - editor.putString("publicKey", encodedPublicKey); - } catch (Exception e) { - Log.e("KDE/PairingDone", "Error encoding public key", e); - } - } - 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); } } diff --git a/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackLink.java b/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackLink.java index 72c94692..bb8bef66 100644 --- a/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackLink.java +++ b/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackLink.java @@ -1,65 +1,59 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Backends.LoopbackBackend; import android.content.Context; 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; -import java.security.PublicKey; - public class LoopbackLink extends BaseLink { public LoopbackLink(Context context, BaseLinkProvider linkProvider) { super(context, "loopback", linkProvider); } @Override public String getName() { return "LoopbackLink"; } @Override public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { return new LoopbackPairingHandler(device, callback); } @Override public boolean sendPacket(NetworkPacket in, Device.SendPacketStatusCallback callback) { packageReceived(in); if (in.hasPayload()) { callback.onProgressChanged(0); in.setPayload(in.getPayload()); callback.onProgressChanged(100); } callback.onSuccess(); return true; } - @Override - public boolean sendPacketEncrypted(NetworkPacket np, Device.SendPacketStatusCallback callback, PublicKey key) { - return sendPacket(np, callback); - } } diff --git a/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackPairingHandler.java b/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackPairingHandler.java index 8b8c0d4c..6d1e0746 100644 --- a/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackPairingHandler.java +++ b/src/org/kde/kdeconnect/Backends/LoopbackBackend/LoopbackPairingHandler.java @@ -1,64 +1,64 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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 packageReceived(NetworkPacket np) throws Exception { + public void packageReceived(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 rejectPairing() { Log.i("LoopbackPairing", "rejectPairing"); mCallback.unpaired(); } @Override public void unpair() { Log.i("LoopbackPairing", "unpair"); mCallback.unpaired(); } } diff --git a/src/org/kde/kdeconnect/Device.java b/src/org/kde/kdeconnect/Device.java index 811379e9..a0b2dcd2 100644 --- a/src/org/kde/kdeconnect/Device.java +++ b/src/org/kde/kdeconnect/Device.java @@ -1,879 +1,843 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BasePairingHandler; -import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; 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_tp.R; import java.security.KeyFactory; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.cert.Certificate; import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; public class Device implements BaseLink.PacketReceiver { private final Context context; private final String deviceId; private String name; - public PublicKey publicKey; public Certificate certificate; private int notificationId; private int protocolVersion; private DeviceType deviceType; private PairStatus pairStatus; private final CopyOnWriteArrayList pairingCallback = new CopyOnWriteArrayList<>(); private final Map pairingHandlers = new HashMap<>(); private final CopyOnWriteArrayList links = new CopyOnWriteArrayList<>(); private List m_supportedPlugins = new ArrayList<>(); private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutPermissions = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>(); private Map> pluginsByIncomingInterface = new HashMap<>(); private final SharedPreferences settings; private final CopyOnWriteArrayList pluginsChangedListeners = new CopyOnWriteArrayList<>(); public interface PluginsChangedListener { void onPluginsChanged(Device device); } public enum PairStatus { NotPaired, Paired } public enum DeviceType { Phone, Tablet, Computer, Tv; static DeviceType FromString(String s) { if ("tablet".equals(s)) return Tablet; if ("phone".equals(s)) return Phone; if ("tv".equals(s)) return Tv; return Computer; //Default } public String toString() { switch (this) { case Tablet: return "tablet"; case Phone: return "phone"; case Tv: return "tv"; default: return "desktop"; } } } 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"); this.context = context; this.deviceId = deviceId; this.name = settings.getString("deviceName", context.getString(R.string.unknown_device)); this.pairStatus = PairStatus.Paired; this.protocolVersion = NetworkPacket.ProtocolVersion; //We don't know it yet this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop")); - try { - String publicKeyStr = settings.getString("publicKey", null); - if (publicKeyStr != null) { - byte[] publicKeyBytes = Base64.decode(publicKeyStr, 0); - publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes)); - } - } catch (Exception e) { - Log.e("KDE/Device", "Exception deserializing stored public key for device", e); - } - //Assume every plugin is supported until addLink is called and we can get the actual list m_supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); //Do not load plugins yet, the device is not present //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"); 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.deviceType = DeviceType.Computer; - this.publicKey = null; settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); addLink(np, dl); } public String getName() { return name != null ? name : context.getString(R.string.unknown_device); } public Drawable getIcon() { int drawableId; switch (deviceType) { case Phone: drawableId = R.drawable.ic_device_phone; break; case Tablet: drawableId = R.drawable.ic_device_tablet; break; case Tv: drawableId = R.drawable.ic_device_tv; break; default: drawableId = R.drawable.ic_device_laptop; } return ContextCompat.getDrawable(context, drawableId); } public DeviceType getDeviceType() { return deviceType; } public String getDeviceId() { return deviceId; } public Context getContext() { return context; } //Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer public int compareProtocolVersion() { return protocolVersion - NetworkPacket.ProtocolVersion; } // // Pairing-related functions // public boolean isPaired() { return pairStatus == PairStatus.Paired; } /* Asks all pairing handlers that, is pair requested? */ public boolean isPairRequested() { boolean pairRequested = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequested = pairRequested || ph.isPairRequested(); } return pairRequested; } /* Asks all pairing handlers that, is pair requested by peer? */ public boolean isPairRequestedByPeer() { boolean pairRequestedByPeer = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequestedByPeer = pairRequestedByPeer || ph.isPairRequestedByPeer(); } return pairRequestedByPeer; } public void addPairingCallback(PairingCallback callback) { pairingCallback.add(callback); } public void removePairingCallback(PairingCallback callback) { pairingCallback.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(); } } 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 package, instead it unpairs internally by deleting trusted device info. . Likely to be called after sending package 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(); } } /* 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(); } } /* This method is called after rejecting pairing from GUI */ public void rejectPairing() { Log.i("KDE/Device", "Rejected pair request started by the other device"); //Log.e("Device","Unpairing (rejectPairing)"); pairStatus = PairStatus.NotPaired; for (BasePairingHandler ph : pairingHandlers.values()) { ph.rejectPairing(); } for (PairingCallback cb : pairingCallback) { cb.pairingFailed(context.getString(R.string.error_canceled_by_user)); } } // // Notification related methods used during pairing // public int getNotificationId() { return notificationId; } public void displayPairingNotification() { hidePairingNotification(); notificationId = (int) System.currentTimeMillis(); Intent intent = new Intent(getContext(), MainActivity.class); intent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); intent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_PENDING); PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 1, intent, PendingIntent.FLAG_CANCEL_CURRENT); Intent acceptIntent = new Intent(getContext(), MainActivity.class); Intent rejectIntent = new Intent(getContext(), MainActivity.class); acceptIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); //acceptIntent.putExtra("notificationId", notificationId); acceptIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_ACCEPTED); rejectIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); //rejectIntent.putExtra("notificationId", notificationId); rejectIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_REJECTED); PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent, PendingIntent.FLAG_ONE_SHOT); PendingIntent rejectedPendingIntent = PendingIntent.getActivity(getContext(), 4, rejectIntent, PendingIntent.FLAG_ONE_SHOT); Resources res = getContext().getResources(); final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT) .setContentTitle(res.getString(R.string.pairing_request_from, getName())) .setContentText(res.getString(R.string.tap_to_answer)) .setContentIntent(pendingIntent) .setTicker(res.getString(R.string.pair_requested)) .setSmallIcon(R.drawable.ic_notification) .addAction(R.drawable.ic_accept_pairing, res.getString(R.string.pairing_accept), acceptedPendingIntent) .addAction(R.drawable.ic_reject_pairing, res.getString(R.string.pairing_reject), rejectedPendingIntent) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .build(); NotificationHelper.notifyCompat(notificationManager, notificationId, noti); BackgroundService.addGuiInUseCounter(context); } public void hidePairingNotification() { final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(notificationId); BackgroundService.removeGuiInUseCounter(context); } // // ComputerLink-related functions // public boolean isReachable() { return !links.isEmpty(); } public void addLink(NetworkPacket identityPacket, BaseLink link) { //FilesHelper.LogOpenFileCount(); this.protocolVersion = identityPacket.getInt("protocolVersion"); if (identityPacket.has("deviceName")) { this.name = identityPacket.getString("deviceName", this.name); SharedPreferences.Editor editor = settings.edit(); editor.putString("deviceName", this.name); editor.apply(); } if (identityPacket.has("deviceType")) { 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); } } links.add(link); try { SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context); byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0); PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); link.setPrivateKey(privateKey); } catch (Exception e) { Log.e("KDE/Device", "Exception reading our own private key", e); //Should not happen } 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 outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null); Set incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null); if (incomingCapabilities != null && outgoingCapabilities != null) { m_supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(incomingCapabilities, outgoingCapabilities)); } else { m_supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); } link.addPacketReceiver(this); reloadPluginsFromSettings(); } 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()); if (links.isEmpty()) { reloadPluginsFromSettings(); } } @Override public void onPacketReceived(NetworkPacket np) { - hackToMakeRetrocompatiblePacketTypes(np); - if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) { Log.i("KDE/Device", "Pair package"); for (BasePairingHandler ph : pairingHandlers.values()) { try { ph.packageReceived(np); } catch (Exception e) { Log.e("PairingPacketReceived", "Exception", e); } } } else if (isPaired()) { // pluginsByIncomingInterface may not be built yet if(pluginsByIncomingInterface.isEmpty()) { reloadPluginsFromSettings(); } //If capabilities are not supported, iterate all plugins Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (targetPlugins != null && !targetPlugins.isEmpty()) { for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onPacketReceived(np); } catch (Exception e) { Log.e("KDE/Device", "Exception in " + plugin.getPluginKey() + "'s onPacketReceived()", e); //try { Log.e("KDE/Device", "NetworkPacket:" + np.serialize()); } catch (Exception _) { } } } } else { Log.w("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } else { //Log.e("KDE/onPacketReceived","Device not paired, will pass package to unpairedPacketListeners"); // If it is pair package, it should be captured by "if" at start // If not and device is paired, it should be captured by isPaired // Else unpair, this handles the situation when one device unpairs, but other dont know like unpairing when wi-fi is off unpair(); //If capabilities are not supported, iterate all plugins Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (targetPlugins != null && !targetPlugins.isEmpty()) { for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onUnpairedDevicePacketReceived(np); } catch (Exception e) { Log.e("KDE/Device", "Exception in " + plugin.getDisplayName() + "'s onPacketReceived() in unPairedPacketListeners", e); } } } else { Log.e("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } } public static abstract class SendPacketStatusCallback { public abstract void onSuccess(); public abstract void onFailure(Throwable e); public void onProgressChanged(int percent) { } } private final SendPacketStatusCallback defaultCallback = new SendPacketStatusCallback() { @Override public void onSuccess() { } @Override public void onFailure(Throwable e) { Log.e("KDE/sendPacket", "Exception", e); } }; public void sendPacket(NetworkPacket np) { sendPacket(np, defaultCallback); } public boolean sendPacketBlocking(NetworkPacket np) { return sendPacketBlocking(np, defaultCallback); } //Async public void sendPacket(final NetworkPacket np, final SendPacketStatusCallback callback) { new Thread(() -> sendPacketBlocking(np, callback)).start(); } public boolean sendPacketBlocking(final NetworkPacket np, final SendPacketStatusCallback callback) { /* if (!m_outgoingCapabilities.contains(np.getType()) && !NetworkPacket.protocolPacketTypes.contains(np.getType())) { Log.e("Device/sendPacket", "Plugin tried to send an undeclared package: " + np.getType()); Log.w("Device/sendPacket", "Declared outgoing package types: " + Arrays.toString(m_outgoingCapabilities.toArray())); } */ - hackToMakeRetrocompatiblePacketTypes(np); - - boolean useEncryption = (protocolVersion < LanLinkProvider.MIN_VERSION_WITH_SSL_SUPPORT && (!np.getType().equals(NetworkPacket.PACKET_TYPE_PAIR) && isPaired())); - boolean success = false; //Make a copy to avoid concurrent modification exception if the original list changes for (final BaseLink link : links) { if (link == null) continue; //Since we made a copy, maybe somebody destroyed the link in the meanwhile - if (useEncryption) { - success = link.sendPacketEncrypted(np, callback, publicKey); - } else { - success = link.sendPacket(np, callback); - } + success = link.sendPacket(np, callback); if (success) break; //If the link didn't call sendSuccess(), try the next one } if (!success) { Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the package. Packet " + np.getType() + " to " + name + " lost!"); } return success; } // // Plugin-related functions // public T getPlugin(Class pluginClass) { Plugin plugin = getPlugin(Plugin.getPluginKey(pluginClass)); return (T) plugin; } public Plugin getPlugin(String pluginKey) { return plugins.get(pluginKey); } private synchronized boolean addPlugin(final String pluginKey) { Plugin existing = plugins.get(pluginKey); if (existing != null) { if (existing.getMinSdk() > Build.VERSION.SDK_INT) { Log.i("KDE/addPlugin", "Min API level not fulfilled " + pluginKey); return false; } //Log.w("KDE/addPlugin","plugin already present:" + pluginKey); if (existing.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, existing); } return true; } final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, pluginKey, this); if (plugin == null) { Log.e("KDE/addPlugin", "could not instantiate plugin: " + pluginKey); return false; } if (plugin.getMinSdk() > Build.VERSION.SDK_INT) { Log.i("KDE/addPlugin", "Min API level not fulfilled" + pluginKey); return false; } boolean success; try { success = plugin.onCreate(); } catch (Exception e) { success = false; Log.e("KDE/addPlugin", "plugin failed to load " + pluginKey, e); } plugins.put(pluginKey, plugin); if (!plugin.checkRequiredPermissions()) { Log.e("KDE/addPlugin", "No permission " + pluginKey); plugins.remove(pluginKey); pluginsWithoutPermissions.put(pluginKey, plugin); success = false; } else { Log.i("KDE/addPlugin", "Permissions OK " + pluginKey); pluginsWithoutPermissions.remove(pluginKey); if (plugin.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, plugin); } } return success; } private synchronized boolean removePlugin(String pluginKey) { Plugin plugin = plugins.remove(pluginKey); if (plugin == null) { return false; } try { plugin.onDestroy(); //Log.e("removePlugin","removed " + pluginKey); } catch (Exception e) { Log.e("KDE/removePlugin", "Exception calling onDestroy for plugin " + pluginKey, e); } return true; } public void setPluginEnabled(String pluginKey, boolean value) { settings.edit().putBoolean(pluginKey, value).apply(); reloadPluginsFromSettings(); } public boolean isPluginEnabled(String pluginKey) { boolean enabledByDefault = PluginFactory.getPluginInfo(pluginKey).isEnabledByDefault(); return settings.getBoolean(pluginKey, enabledByDefault); } public void reloadPluginsFromSettings() { HashMap> newPluginsByIncomingInterface = new HashMap<>(); for (String pluginKey : m_supportedPlugins) { PluginFactory.PluginInfo pluginInfo = PluginFactory.getPluginInfo(pluginKey); boolean pluginEnabled = false; boolean listenToUnpaired = pluginInfo.listenToUnpaired(); if ((isPaired() || listenToUnpaired) && isReachable()) { pluginEnabled = isPluginEnabled(pluginKey); } if (pluginEnabled) { boolean success = addPlugin(pluginKey); if (success) { for (String packageType : pluginInfo.getSupportedPacketTypes()) { - packageType = hackToMakeRetrocompatiblePacketTypes(packageType); ArrayList plugins = newPluginsByIncomingInterface.get(packageType); if (plugins == null) plugins = new ArrayList<>(); plugins.add(pluginKey); newPluginsByIncomingInterface.put(packageType, plugins); } } } else { removePlugin(pluginKey); } } pluginsByIncomingInterface = newPluginsByIncomingInterface; onPluginsChanged(); } public void onPluginsChanged() { for (PluginsChangedListener listener : pluginsChangedListeners) { listener.onPluginsChanged(Device.this); } } public ConcurrentHashMap getLoadedPlugins() { return plugins; } public ConcurrentHashMap getPluginsWithoutPermissions() { return pluginsWithoutPermissions; } public ConcurrentHashMap getPluginsWithoutOptionalPermissions() { return pluginsWithoutOptionalPermissions; } public void addPluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.add(listener); } public void removePluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.remove(listener); } public void disconnect() { for (BaseLink link : links) { link.disconnect(); } } public boolean deviceShouldBeKeptAlive() { SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); if (preferences.contains(getDeviceId())) { //Log.e("DeviceShouldBeKeptAlive", "because it's a paired device"); return true; //Already paired } for (BaseLink l : links) { if (l.linkShouldBeKeptAlive()) { return true; } } return false; } public List getSupportedPlugins() { return m_supportedPlugins; } - private void hackToMakeRetrocompatiblePacketTypes(NetworkPacket np) { - if (protocolVersion >= 6) return; - np.mType = np.getType().replace(".request", ""); - } - - private String hackToMakeRetrocompatiblePacketTypes(String type) { - if (protocolVersion >= 6) return type; - return type.replace(".request", ""); - } - } diff --git a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/RsaHelper.java b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/RsaHelper.java index 3244f375..40bd1e0b 100644 --- a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/RsaHelper.java +++ b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/RsaHelper.java @@ -1,143 +1,81 @@ /* * Copyright 2015 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Helpers.SecurityHelpers; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; -import org.json.JSONArray; -import org.json.JSONException; -import org.kde.kdeconnect.NetworkPacket; - -import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; -import javax.crypto.Cipher; - public class RsaHelper { public static void initialiseRsaKeys(Context context) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); if (!settings.contains("publicKey") || !settings.contains("privateKey")) { KeyPair keyPair; try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); keyPair = keyGen.genKeyPair(); } catch (Exception e) { Log.e("KDE/initializeRsaKeys", "Exception", e); return; } byte[] publicKey = keyPair.getPublic().getEncoded(); byte[] privateKey = keyPair.getPrivate().getEncoded(); SharedPreferences.Editor edit = settings.edit(); edit.putString("publicKey", Base64.encodeToString(publicKey, 0).trim() + "\n"); edit.putString("privateKey", Base64.encodeToString(privateKey, 0)); edit.apply(); } } public static PublicKey getPublicKey(Context context) throws GeneralSecurityException { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); byte[] publicKeyBytes = Base64.decode(settings.getString("publicKey", ""), 0); return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes)); } - public static PublicKey getPublicKey(Context context, String deviceId) throws GeneralSecurityException { - SharedPreferences settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); - byte[] publicKeyBytes = Base64.decode(settings.getString("publicKey", ""), 0); - return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes)); - } - public static PrivateKey getPrivateKey(Context context) throws GeneralSecurityException { SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context); byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0); return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); } - public static NetworkPacket encrypt(NetworkPacket np, PublicKey publicKey) throws GeneralSecurityException, JSONException { - - String serialized = np.serialize(); - - int chunkSize = 128; - - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - - JSONArray chunks = new JSONArray(); - while (serialized.length() > 0) { - if (serialized.length() < chunkSize) { - chunkSize = serialized.length(); - } - String chunk = serialized.substring(0, chunkSize); - serialized = serialized.substring(chunkSize); - byte[] chunkBytes = chunk.getBytes(Charset.defaultCharset()); - byte[] encryptedChunk; - encryptedChunk = cipher.doFinal(chunkBytes); - chunks.put(Base64.encodeToString(encryptedChunk, Base64.NO_WRAP)); - } - - //Log.i("NetworkPacket", "Encrypted " + chunks.length()+" chunks"); - - NetworkPacket encrypted = new NetworkPacket(NetworkPacket.PACKET_TYPE_ENCRYPTED); - encrypted.set("data", chunks); - encrypted.setPayload(np.getPayload()); - return encrypted; - - } - - public static NetworkPacket decrypt(NetworkPacket np, PrivateKey privateKey) throws GeneralSecurityException, JSONException { - - JSONArray chunks = np.getJSONArray("data"); - - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - - StringBuilder decryptedJson = new StringBuilder(); - for (int i = 0; i < chunks.length(); i++) { - byte[] encryptedChunk = Base64.decode(chunks.getString(i), Base64.NO_WRAP); - String decryptedChunk = new String(cipher.doFinal(encryptedChunk)); - decryptedJson.append(decryptedChunk); - } - - NetworkPacket decrypted = NetworkPacket.unserialize(decryptedJson.toString()); - decrypted.setPayload(np.getPayload()); - return decrypted; - } } diff --git a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java index 98b96074..84349838 100644 --- a/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java +++ b/src/org/kde/kdeconnect/Helpers/SecurityHelpers/SslHelper.java @@ -1,255 +1,250 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Helpers.SecurityHelpers; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.RandomHelper; import org.spongycastle.asn1.x500.X500NameBuilder; import org.spongycastle.asn1.x500.style.BCStyle; import org.spongycastle.cert.X509CertificateHolder; import org.spongycastle.cert.X509v3CertificateBuilder; import org.spongycastle.cert.jcajce.JcaX509CertificateConverter; import org.spongycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.operator.ContentSigner; import org.spongycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.IOException; import java.math.BigInteger; import java.net.Socket; import java.net.SocketException; import java.security.KeyStore; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Formatter; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class SslHelper { - public enum SslMode { - Client, - Server - } - public static X509Certificate certificate; //my device's certificate public static final BouncyCastleProvider BC = new BouncyCastleProvider(); public static void initialiseCertificate(Context context) { PrivateKey privateKey; PublicKey publicKey; try { privateKey = RsaHelper.getPrivateKey(context); publicKey = RsaHelper.getPublicKey(context); } catch (Exception e) { Log.e("SslHelper", "Error getting keys, can't create certificate"); return; } SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); if (!settings.contains("certificate")) { try { X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); nameBuilder.addRDN(BCStyle.CN, DeviceHelper.getDeviceId(context)); nameBuilder.addRDN(BCStyle.OU, "KDE Connect"); nameBuilder.addRDN(BCStyle.O, "KDE"); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, -1); Date notBefore = calendar.getTime(); calendar.add(Calendar.YEAR, 10); Date notAfter = calendar.getTime(); X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder( nameBuilder.build(), BigInteger.ONE, notBefore, notAfter, nameBuilder.build(), publicKey ); ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider(BC).build(privateKey); certificate = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateBuilder.build(contentSigner)); SharedPreferences.Editor edit = settings.edit(); edit.putString("certificate", Base64.encodeToString(certificate.getEncoded(), 0)); edit.apply(); } catch (Exception e) { Log.e("KDE/initialiseCert", "Exception", e); } } else { try { SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context); byte[] certificateBytes = Base64.decode(globalSettings.getString("certificate", ""), 0); X509CertificateHolder certificateHolder = new X509CertificateHolder(certificateBytes); certificate = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder); } catch (Exception e) { Log.e("KDE/SslHelper", "Exception reading own certificate", e); } } } public static boolean isCertificateStored(Context context, String deviceId) { SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); String cert = devicePreferences.getString("certificate", ""); return !cert.isEmpty(); } private static SSLContext getSslContext(Context context, String deviceId, boolean isDeviceTrusted) { //TODO: Cache try { // Get device private key PrivateKey privateKey = RsaHelper.getPrivateKey(context); // Get remote device certificate if trusted X509Certificate remoteDeviceCertificate = null; if (isDeviceTrusted) { SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); byte[] certificateBytes = Base64.decode(devicePreferences.getString("certificate", ""), 0); X509CertificateHolder certificateHolder = new X509CertificateHolder(certificateBytes); remoteDeviceCertificate = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder); } // 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) { keyStore.setCertificateEntry(deviceId, remoteDeviceCertificate); } // Setup key manager factory KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, "".toCharArray()); // Setup default trust manager TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); // Setup custom trust manager if device not trusted TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } @Override public void checkClientTrusted(X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; SSLContext tlsContext = SSLContext.getInstance("TLSv1"); //Newer TLS versions are only supported on API 16+ if (isDeviceTrusted) { tlsContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), RandomHelper.secureRandom); } else { tlsContext.init(keyManagerFactory.getKeyManagers(), trustAllCerts, RandomHelper.secureRandom); } return tlsContext; } catch (Exception e) { Log.e("KDE/SslHelper", "Error creating tls context", e); } return null; } private static void configureSslSocket(SSLSocket socket, boolean isDeviceTrusted, boolean isClient) throws SocketException { // These cipher suites are most common of them that are accepted by kde and android during handshake ArrayList supportedCiphers = new ArrayList<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { supportedCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); // API 20+ supportedCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); // API 20+ } supportedCiphers.add("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); // API 11+ socket.setEnabledCipherSuites(supportedCiphers.toArray(new String[0])); socket.setSoTimeout(10000); if (isClient) { socket.setUseClientMode(true); } else { socket.setUseClientMode(false); if (isDeviceTrusted) { socket.setNeedClientAuth(true); } else { socket.setWantClientAuth(true); } } } public static SSLSocket convertToSslSocket(Context context, Socket socket, String deviceId, boolean isDeviceTrusted, boolean clientMode) throws IOException { SSLSocketFactory sslsocketFactory = SslHelper.getSslContext(context, deviceId, isDeviceTrusted).getSocketFactory(); SSLSocket sslsocket = (SSLSocket) sslsocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); SslHelper.configureSslSocket(sslsocket, isDeviceTrusted, clientMode); return sslsocket; } public static String getCertificateHash(Certificate certificate) { try { byte[] hash = MessageDigest.getInstance("SHA-1").digest(certificate.getEncoded()); Formatter formatter = new Formatter(); int i; for (i = 0; i < hash.length - 1; i++) { formatter.format("%02x:", hash[i]); } formatter.format("%02x", hash[i]); return formatter.toString(); } catch (Exception e) { return null; } } public static Certificate parseCertificate(byte[] certificateBytes) throws IOException, CertificateException { X509CertificateHolder certificateHolder = new X509CertificateHolder(certificateBytes); return new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder); } } diff --git a/src/org/kde/kdeconnect/NetworkPacket.java b/src/org/kde/kdeconnect/NetworkPacket.java index 87ccdc07..84824acd 100644 --- a/src/org/kde/kdeconnect/NetworkPacket.java +++ b/src/org/kde/kdeconnect/NetworkPacket.java @@ -1,368 +1,366 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect; import android.content.Context; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Plugins.PluginFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class NetworkPacket { public final static int ProtocolVersion = 7; public final static String PACKET_TYPE_IDENTITY = "kdeconnect.identity"; public final static String PACKET_TYPE_PAIR = "kdeconnect.pair"; - public final static String PACKET_TYPE_ENCRYPTED = "kdeconnect.encrypted"; public static Set protocolPacketTypes = new HashSet() {{ add(PACKET_TYPE_IDENTITY); add(PACKET_TYPE_PAIR); - add(PACKET_TYPE_ENCRYPTED); }}; private long mId; String mType; private JSONObject mBody; private Payload mPayload; private JSONObject mPayloadTransferInfo; private volatile boolean canceled; private NetworkPacket() { } public NetworkPacket(String type) { mId = System.currentTimeMillis(); mType = type; mBody = new JSONObject(); mPayload = null; mPayloadTransferInfo = new JSONObject(); } public boolean isCanceled() { return canceled; } public void cancel() { canceled = true; } public String getType() { return mType; } public long getId() { return mId; } //Most commons getters and setters defined for convenience public String getString(String key) { return mBody.optString(key, ""); } public String getString(String key, String defaultValue) { return mBody.optString(key, defaultValue); } public void set(String key, String value) { if (value == null) return; try { mBody.put(key, value); } catch (Exception ignored) { } } public int getInt(String key) { return mBody.optInt(key, -1); } public int getInt(String key, int defaultValue) { return mBody.optInt(key, defaultValue); } public long getLong(String key) { return mBody.optLong(key, -1); } public long getLong(String key, long defaultValue) { return mBody.optLong(key, defaultValue); } public void set(String key, int value) { try { mBody.put(key, value); } catch (Exception ignored) { } } public boolean getBoolean(String key) { return mBody.optBoolean(key, false); } public boolean getBoolean(String key, boolean defaultValue) { return mBody.optBoolean(key, defaultValue); } public void set(String key, boolean value) { try { mBody.put(key, value); } catch (Exception ignored) { } } public double getDouble(String key) { return mBody.optDouble(key, Double.NaN); } public double getDouble(String key, double defaultValue) { return mBody.optDouble(key, defaultValue); } public void set(String key, double value) { try { mBody.put(key, value); } catch (Exception ignored) { } } public JSONArray getJSONArray(String key) { return mBody.optJSONArray(key); } public void set(String key, JSONArray value) { try { mBody.put(key, value); } catch (Exception ignored) { } } public JSONObject getJSONObject(String key) { return mBody.optJSONObject(key); } public void set(String key, JSONObject value) { try { mBody.put(key, value); } catch (JSONException ignored) { } } private Set getStringSet(String key) { JSONArray jsonArray = mBody.optJSONArray(key); if (jsonArray == null) return null; Set list = new HashSet<>(); int length = jsonArray.length(); for (int i = 0; i < length; i++) { try { String str = jsonArray.getString(i); list.add(str); } catch (Exception ignored) { } } return list; } public Set getStringSet(String key, Set defaultValue) { if (mBody.has(key)) return getStringSet(key); else return defaultValue; } public void set(String key, Set value) { try { JSONArray jsonArray = new JSONArray(); for (String str : value) { jsonArray.put(str); } mBody.put(key, jsonArray); } catch (Exception ignored) { } } public List getStringList(String key) { JSONArray jsonArray = mBody.optJSONArray(key); if (jsonArray == null) return null; List list = new ArrayList<>(); int length = jsonArray.length(); for (int i = 0; i < length; i++) { try { String str = jsonArray.getString(i); list.add(str); } catch (Exception ignored) { } } return list; } public List getStringList(String key, List defaultValue) { if (mBody.has(key)) return getStringList(key); else return defaultValue; } public void set(String key, List value) { try { JSONArray jsonArray = new JSONArray(); for (String str : value) { jsonArray.put(str); } mBody.put(key, jsonArray); } catch (Exception ignored) { } } public boolean has(String key) { return mBody.has(key); } public String serialize() throws JSONException { JSONObject jo = new JSONObject(); jo.put("id", mId); jo.put("type", mType); jo.put("body", mBody); if (hasPayload()) { jo.put("payloadSize", mPayload.payloadSize); jo.put("payloadTransferInfo", mPayloadTransferInfo); } //QJSon does not escape slashes, but Java JSONObject does. Converting to QJson format. return jo.toString().replace("\\/", "/") + "\n"; } static public NetworkPacket unserialize(String s) throws JSONException { NetworkPacket np = new NetworkPacket(); JSONObject jo = new JSONObject(s); np.mId = jo.getLong("id"); np.mType = jo.getString("type"); np.mBody = jo.getJSONObject("body"); if (jo.has("payloadSize")) { np.mPayloadTransferInfo = jo.getJSONObject("payloadTransferInfo"); np.mPayload = new Payload(jo.getLong("payloadSize")); } else { np.mPayloadTransferInfo = new JSONObject(); np.mPayload = new Payload(0); } return np; } static public NetworkPacket createIdentityPacket(Context context) { NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY); String deviceId = DeviceHelper.getDeviceId(context); try { np.mBody.put("deviceId", deviceId); np.mBody.put("deviceName", DeviceHelper.getDeviceName(context)); np.mBody.put("protocolVersion", NetworkPacket.ProtocolVersion); np.mBody.put("deviceType", DeviceHelper.getDeviceType(context).toString()); np.mBody.put("incomingCapabilities", new JSONArray(PluginFactory.getIncomingCapabilities())); np.mBody.put("outgoingCapabilities", new JSONArray(PluginFactory.getOutgoingCapabilities())); } catch (Exception e) { Log.e("NetworkPackage", "Exception on createIdentityPacket", e); } return np; } public void setPayload(Payload payload) { mPayload = payload; } public Payload getPayload() { return mPayload; } public long getPayloadSize() { return mPayload == null ? 0 : mPayload.payloadSize; } public boolean hasPayload() { return (mPayload != null && mPayload.payloadSize != 0); } public boolean hasPayloadTransferInfo() { return (mPayloadTransferInfo.length() > 0); } public JSONObject getPayloadTransferInfo() { return mPayloadTransferInfo; } public void setPayloadTransferInfo(JSONObject payloadTransferInfo) { mPayloadTransferInfo = payloadTransferInfo; } public static class Payload { private InputStream inputStream; private Socket inputSocket; private long payloadSize; public Payload(long payloadSize) { this((InputStream)null, payloadSize); } public Payload(byte[] data) { this(new ByteArrayInputStream(data), data.length); } /** * NOTE: Do not use this to set an SSLSockets InputStream as the payload, use Payload(Socket, long) instead because of this bug */ public Payload(InputStream inputStream, long payloadSize) { this.inputSocket = null; this.inputStream = inputStream; this.payloadSize = payloadSize; } public Payload(Socket inputSocket, long payloadSize) throws IOException { this.inputSocket = inputSocket; this.inputStream = inputSocket.getInputStream(); this.payloadSize = payloadSize; } /** * NOTE: Do not close the InputStream directly call Payload.close() instead, this is because of this bug */ public InputStream getInputStream() { return inputStream; } long getPayloadSize() { return payloadSize; } public void close() { //TODO: If socket only close socket if that also closes the streams that is try { if (inputStream != null) { inputStream.close(); } } catch(IOException ignored) {} try { if (inputSocket != null) { inputSocket.close(); } } catch (IOException ignored) {} } } } diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index 9a9e3ab9..d07a56bd 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -1,262 +1,246 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; -import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.AlertDialogFragment; import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; public abstract class Plugin { protected Device device; protected Context context; protected int permissionExplanation = R.string.permission_explanation; protected int optionalPermissionExplanation = R.string.optional_permission_explanation; public final void setContext(Context context, Device device) { this.device = device; this.context = context; } /** * To receive the network package from the unpaired device, override * listensToUnpairedDevices to return true and this method. */ public boolean onUnpairedDevicePacketReceived(NetworkPacket np) { return false; } /** * Returns whether this plugin should be loaded or not, to listen to NetworkPackets * from the unpaired devices. By default, returns false. */ public boolean listensToUnpairedDevices() { return false; } /** * Return the internal plugin name, that will be used as a * unique key to distinguish it. Use the class name as key. */ public String getPluginKey() { return getPluginKey(this.getClass()); } public static String getPluginKey(Class p) { return p.getSimpleName(); } /** * Return the human-readable plugin name. This function can * access this.context to provide translated text. */ public abstract String getDisplayName(); /** * Return the human-readable description of this plugin. This * function can access this.context to provide translated text. */ public abstract String getDescription(); /** * Return the action name displayed in the main activity, that * will call startMainActivity when clicked */ public String getActionName() { return getDisplayName(); } /** * Return an icon associated to this plugin. This function can * access this.context to load the image from resources. */ public Drawable getIcon() { return null; } /** * Return true if this plugin should be enabled on new devices. * This function can access this.context and perform compatibility * checks with the Android version, but can not access this.device. */ public boolean isEnabledByDefault() { return true; } /** * Return true if this plugin needs an specific UI settings. */ public boolean hasSettings() { return false; } /** * If hasSettings returns true, this will be called when the user * wants to access this plugin's preferences. The default implementation * will return a PluginSettingsFragment with content from "yourplugin"_preferences.xml * * @return The PluginSettingsFragment used to display this plugins settings */ public PluginSettingsFragment getSettingsFragment(Activity activity) { return PluginSettingsFragment.newInstance(getPluginKey()); } /** * Return true if the plugin should display something in the Device main view */ public boolean hasMainActivity() { return false; } /** * Implement here what your plugin should do when clicked */ public void startMainActivity(Activity parentActivity) { } /** * Return true if the entry for this app should appear in the context menu instead of the main view */ public boolean displayInContextMenu() { return false; } /** * Initialize the listeners and structures in your plugin. * Should return true if initialization was successful. */ public boolean onCreate() { return true; } /** * Finish any ongoing operations, remove listeners... so * this object could be garbage collected. */ public void onDestroy() { } /** * Called when a plugin receives a package. By convention we return true * when we have done something in response to the package or false * otherwise, even though that value is unused as of now. */ public boolean onPacketReceived(NetworkPacket np) { return false; } /** * Should return the list of NetworkPacket types that this plugin can handle */ public abstract String[] getSupportedPacketTypes(); /** * Should return the list of NetworkPacket types that this plugin can send */ public abstract String[] getOutgoingPacketTypes(); - /** - * Creates a button that will be displayed in the user interface - * It can open an activity or perform any other action that the - * plugin would wants to expose to the user. Return null if no - * button should be displayed. - */ - @Deprecated - public Button getInterfaceButton(final Activity activity) { - if (!hasMainActivity()) return null; - Button b = new Button(activity); - b.setText(getActionName()); - b.setOnClickListener(view -> startMainActivity(activity)); - return b; - } - protected String[] getRequiredPermissions() { return new String[0]; } protected String[] getOptionalPermissions() { return new String[0]; } //Permission from Manifest.permission.* protected boolean isPermissionGranted(String permission) { int result = ContextCompat.checkSelfPermission(context, permission); return (result == PackageManager.PERMISSION_GRANTED); } private boolean arePermissionsGranted(String[] permissions) { for (String permission : permissions) { if (!isPermissionGranted(permission)) { return false; } } return true; } private PermissionsAlertDialogFragment requestPermissionDialog(final String[] permissions, @StringRes int reason, int requestCode) { return new PermissionsAlertDialogFragment.Builder() .setTitle(getDisplayName()) .setMessage(reason) .setPositiveButton(R.string.ok) .setNegativeButton(R.string.cancel) .setPermissions(permissions) .setRequestCode(requestCode) .create(); } /** * If onCreate returns false, should create a dialog explaining * the problem (and how to fix it, if possible) to the user. */ public AlertDialogFragment getPermissionExplanationDialog(int requestCode) { return requestPermissionDialog(getRequiredPermissions(), permissionExplanation, requestCode); } public AlertDialogFragment getOptionalPermissionExplanationDialog(int requestCode) { return requestPermissionDialog(getOptionalPermissions(), optionalPermissionExplanation, requestCode); } public boolean checkRequiredPermissions() { return arePermissionsGranted(getRequiredPermissions()); } public boolean checkOptionalPermissions() { return arePermissionsGranted(getOptionalPermissions()); } public int getMinSdk() { return Build.VERSION_CODES.BASE; } } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java index 9901a4a6..30a2a712 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/AndroidSafSshFile.java @@ -1,499 +1,499 @@ /* * Copyright 2018 Erik Duisters * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SftpPlugin; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import org.apache.sshd.common.file.SshFile; import org.kde.kdeconnect.Helpers.FilesHelper; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import androidx.annotation.Nullable; @TargetApi(21) public class AndroidSafSshFile implements SshFile { private static final String TAG = AndroidSafSshFile.class.getSimpleName(); private final String virtualFileName; private DocumentInfo documentInfo; private Uri parentUri; private final AndroidSafFileSystemView fileSystemView; AndroidSafSshFile(final AndroidSafFileSystemView fileSystemView, Uri parentUri, Uri uri, String virtualFileName) { this.fileSystemView = fileSystemView; this.parentUri = parentUri; this.documentInfo = new DocumentInfo(fileSystemView.context, uri); this.virtualFileName = virtualFileName; } @Override public String getAbsolutePath() { return virtualFileName; } @Override public String getName() { /* From NativeSshFile, looks a lot like new File(virtualFileName).getName() to me */ // strip the last '/' String shortName = virtualFileName; int filelen = virtualFileName.length(); if (shortName.charAt(filelen - 1) == File.separatorChar) { shortName = shortName.substring(0, filelen - 1); } // return from the last '/' int slashIndex = shortName.lastIndexOf(File.separatorChar); if (slashIndex != -1) { shortName = shortName.substring(slashIndex + 1); } return shortName; } @Override public String getOwner() { return fileSystemView.userName; } @Override public boolean isDirectory() { return documentInfo.isDirectory; } @Override public boolean isFile() { return documentInfo.isFile; } @Override public boolean doesExist() { return documentInfo.exists; } @Override public long getSize() { return documentInfo.length; } @Override public long getLastModified() { return documentInfo.lastModified; } @Override public boolean setLastModified(long time) { //TODO /* Throws UnsupportedOperationException on API 26 try { ContentValues updateValues = new ContentValues(); updateValues.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, time); result = fileSystemView.context.getContentResolver().update(documentInfo.uri, updateValues, null, null) != 0; documentInfo.lastModified = time; } catch (NullPointerException ignored) {} */ return true; } @Override public boolean isReadable() { return documentInfo.canRead; } @Override public boolean isWritable() { return documentInfo.canWrite; } @Override public boolean isExecutable() { return documentInfo.isDirectory; } @Override public boolean isRemovable() { Log.d(TAG, "isRemovable() - is this ever called?"); return false; } public SshFile getParentFile() { Log.d(TAG,"getParentFile() - is this ever called"); return null; } @Override public boolean delete() { boolean ret; try { ret = DocumentsContract.deleteDocument(fileSystemView.context.getContentResolver(), documentInfo.uri); } catch (FileNotFoundException e) { ret = false; } return ret; } @Override public boolean create() { return create(parentUri, FilesHelper.getMimeTypeFromFile(virtualFileName), getName()); } private boolean create(Uri parentUri, String mimeType, String name) { Uri uri = null; try { uri = DocumentsContract.createDocument(fileSystemView.context.getContentResolver(), parentUri, mimeType, name); if (uri != null) { documentInfo = new DocumentInfo(fileSystemView.context, uri); } } catch (FileNotFoundException ignored) {} return uri != null; } @Override public void truncate() { if (documentInfo.length > 0) { delete(); create(); } } @Override public boolean move(final SshFile dest) { boolean success = false; Uri destParentUri = ((AndroidSafSshFile)dest).parentUri; if (destParentUri.equals(parentUri)) { //Rename try { Uri newUri = DocumentsContract.renameDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, dest.getName()); if (newUri != null) { success = true; documentInfo.uri = newUri; } } catch (FileNotFoundException ignored) {} } else { // Move: String sourceTreeDocumentId = DocumentsContract.getTreeDocumentId(parentUri); String destTreeDocumentId = DocumentsContract.getTreeDocumentId(((AndroidSafSshFile) dest).parentUri); if (sourceTreeDocumentId.equals(destTreeDocumentId) && Build.VERSION.SDK_INT >= 24) { try { Uri newUri = DocumentsContract.moveDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, parentUri, destParentUri); if (newUri != null) { success = true; parentUri = destParentUri; documentInfo.uri = newUri; } } catch (Exception e) { Log.e(TAG,"DocumentsContract.moveDocument() threw an exception", e); } } else { try { if (dest.create()) { try (InputStream in = createInputStream(0); OutputStream out = dest.createOutputStream(0)) { byte[] buffer = new byte[10 * 1024]; int read; while ((read = in.read(buffer)) > 0) { out.write(buffer, 0, read); } out.flush(); delete(); success = true; } catch (IOException e) { if (dest.doesExist()) { dest.delete(); } } } } catch (IOException ignored) {} } } return success; } @Override public boolean mkdir() { return create(parentUri, DocumentsContract.Document.MIME_TYPE_DIR, getName()); } @Override public List listSshFiles() { if (!documentInfo.isDirectory) { return null; } final ContentResolver resolver = fileSystemView.context.getContentResolver(); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(documentInfo.uri, DocumentsContract.getDocumentId(documentInfo.uri)); final ArrayList results = new ArrayList<>(); Cursor c = resolver.query(childrenUri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null); while (c != null && c.moveToNext()) { final String documentId = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)); final String displayName = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)); final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(documentInfo.uri, documentId); results.add(new AndroidSafSshFile(fileSystemView, parentUri, documentUri, virtualFileName + File.separatorChar + displayName)); } if (c != null) { c.close(); } return Collections.unmodifiableList(results); } @Override public OutputStream createOutputStream(final long offset) throws IOException { return fileSystemView.context.getContentResolver().openOutputStream(documentInfo.uri); } @Override public InputStream createInputStream(final long offset) throws IOException { return fileSystemView.context.getContentResolver().openInputStream(documentInfo.uri); } @Override public void handleClose() { // Nop } @Override - public Map getAttributes(boolean followLinks) throws IOException { + public Map getAttributes(boolean followLinks) { Map attributes = new HashMap<>(); for (SshFile.Attribute attr : SshFile.Attribute.values()) { switch (attr) { case Uid: case Gid: case NLink: continue; } attributes.put(attr, getAttribute(attr, followLinks)); } return attributes; } @Override public Object getAttribute(Attribute attribute, boolean followLinks) { Object ret; switch (attribute) { case Size: ret = documentInfo.length; break; case Uid: ret = 1; break; case Owner: ret = getOwner(); break; case Gid: ret = 1; break; case Group: ret = getOwner(); break; case IsDirectory: ret = documentInfo.isDirectory; break; case IsRegularFile: ret = documentInfo.isFile; break; case IsSymbolicLink: ret = false; break; case Permissions: Set tmp = new HashSet<>(); if (documentInfo.canRead) { tmp.add(SshFile.Permission.UserRead); tmp.add(SshFile.Permission.GroupRead); tmp.add(SshFile.Permission.OthersRead); } if (documentInfo.canWrite) { tmp.add(SshFile.Permission.UserWrite); tmp.add(SshFile.Permission.GroupWrite); tmp.add(SshFile.Permission.OthersWrite); } if (isExecutable()) { tmp.add(SshFile.Permission.UserExecute); tmp.add(SshFile.Permission.GroupExecute); tmp.add(SshFile.Permission.OthersExecute); } ret = tmp.isEmpty() ? EnumSet.noneOf(SshFile.Permission.class) : EnumSet.copyOf(tmp); break; case CreationTime: ret = documentInfo.lastModified; break; case LastModifiedTime: ret = documentInfo.lastModified; break; case LastAccessTime: ret = documentInfo.lastModified; break; case NLink: ret = 0; break; default: ret = null; break; } return ret; } @Override public void setAttributes(Map attributes) { //TODO: Using Java 7 NIO it should be possible to implement setting a number of attributes but does SaF allow that? Log.d(TAG, "setAttributes()"); } @Override public void setAttribute(Attribute attribute, Object value) { Log.d(TAG, "setAttribute()"); } @Override public String readSymbolicLink() throws IOException { throw new IOException("Not Implemented"); } @Override public void createSymbolicLink(SshFile destination) throws IOException { throw new IOException("Not Implemented"); } /** * Retrieve all file info using 1 query to speed things up * The only fields guaranteed to be initialized are uri and exists */ private static class DocumentInfo { private Uri uri; private boolean exists; @Nullable private String documentId; private boolean canRead; private boolean canWrite; @Nullable private String mimeType; private boolean isDirectory; private boolean isFile; private long lastModified; private long length; @Nullable private String displayName; private static final String[] columns; static { columns = new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, //DocumentsContract.Document.COLUMN_ICON, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE }; } /* Based on https://github.com/rcketscientist/DocumentActivity Extracted from android.support.v4.provider.DocumentsContractAPI19 and android.support.v4.provider.DocumentsContractAPI21 */ private DocumentInfo(Context c, Uri uri) { this.uri = uri; try (Cursor cursor = c.getContentResolver().query(uri, columns, null, null, null)) { exists = cursor != null && cursor.getCount() > 0; if (!exists) return; cursor.moveToFirst(); documentId = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)); final boolean readPerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED; final boolean writePerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED; final int flags = cursor.getInt(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS)); final boolean supportsDelete = (flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0; final boolean supportsCreate = (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0; final boolean supportsWrite = (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0; mimeType = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)); final boolean hasMime = !TextUtils.isEmpty(mimeType); isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); isFile = !isDirectory && hasMime; canRead = readPerm && hasMime; canWrite = writePerm && (supportsDelete || (isDirectory && supportsCreate) || (hasMime && supportsWrite)); displayName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)); lastModified = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)); length = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)); } catch (IllegalArgumentException e) { //File does not exist, it's probably going to be created exists = false; canWrite = true; } } } } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java index 17236c25..a6eea5dc 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/RootFile.java @@ -1,178 +1,178 @@ /* * Copyright 2018 Erik Duisters * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SftpPlugin; import org.apache.sshd.common.file.SshFile; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Calendar; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; //TODO: ls .. and ls / only show .. and / respectively I would expect a listing //TODO: cd .. to / does not work and prints "Can't change directory: Can't check target" class RootFile implements SshFile { private final boolean exists; private final String userName; private final List files; RootFile(List files, String userName, boolean exits) { this.files = files; this.userName = userName; this.exists = exits; } public String getAbsolutePath() { return "/"; } public String getName() { return "/"; } - public Map getAttributes(boolean followLinks) throws IOException { + public Map getAttributes(boolean followLinks) { Map attrs = new HashMap<>(); attrs.put(Attribute.Size, 0); attrs.put(Attribute.Owner, userName); attrs.put(Attribute.Group, userName); EnumSet p = EnumSet.noneOf(Permission.class); p.add(Permission.UserExecute); p.add(Permission.GroupExecute); p.add(Permission.OthersExecute); attrs.put(Attribute.Permissions, p); long now = Calendar.getInstance().getTimeInMillis(); attrs.put(Attribute.LastAccessTime, now); attrs.put(Attribute.LastModifiedTime, now); attrs.put(Attribute.IsSymbolicLink, false); attrs.put(Attribute.IsDirectory, true); attrs.put(Attribute.IsRegularFile, false); return attrs; } public void setAttributes(Map attributes) { } public Object getAttribute(Attribute attribute, boolean followLinks) { return null; } public void setAttribute(Attribute attribute, Object value) { } public String readSymbolicLink() { return ""; } public void createSymbolicLink(SshFile destination) { } public String getOwner() { return null; } public boolean isDirectory() { return true; } public boolean isFile() { return false; } public boolean doesExist() { return exists; } public boolean isReadable() { return true; } public boolean isWritable() { return false; } public boolean isExecutable() { return true; } public boolean isRemovable() { return false; } public SshFile getParentFile() { return this; } public long getLastModified() { return 0; } public boolean setLastModified(long time) { return false; } public long getSize() { return 0; } public boolean mkdir() { return false; } public boolean delete() { return false; } public boolean create() { return false; } public void truncate() { } public boolean move(SshFile destination) { return false; } public List listSshFiles() { return Collections.unmodifiableList(files); } public OutputStream createOutputStream(long offset) { return null; } public InputStream createInputStream(long offset) { return null; } public void handleClose() { } } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java index e6184848..e56b6cfc 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java @@ -1,207 +1,204 @@ /* * Copyright 2014 Samoilenko Yuri * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SftpPlugin; import android.content.Context; import android.util.Log; import org.apache.sshd.SshServer; import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; import org.apache.sshd.common.util.SecurityUtils; import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.command.ScpCommandFactory; import org.apache.sshd.server.kex.DHG1; import org.apache.sshd.server.kex.DHG14; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.sftp.SftpSubsystem; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.RandomHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.List; class SimpleSftpServer { private static final int STARTPORT = 1739; private static final int ENDPORT = 1764; static final String USER = "kdeconnect"; private int port = -1; private boolean started = false; private final SimplePasswordAuthenticator passwordAuth = new SimplePasswordAuthenticator(); private final SimplePublicKeyAuthenticator keyAuth = new SimplePublicKeyAuthenticator(); static { Security.insertProviderAt(SslHelper.BC, 1); SecurityUtils.setRegisterBouncyCastle(false); } private final SshServer sshd = SshServer.setUpDefaultServer(); private AndroidFileSystemFactory fileSystemFactory; void init(Context context, Device device) throws GeneralSecurityException { sshd.setKeyExchangeFactories(Arrays.asList( new DHG14.Factory(), new DHG1.Factory())); //Reuse this device keys for the ssh connection as well final KeyPair keyPair; PrivateKey privateKey = RsaHelper.getPrivateKey(context); PublicKey publicKey = RsaHelper.getPublicKey(context); keyPair = new KeyPair(publicKey, privateKey); sshd.setKeyPairProvider(new AbstractKeyPairProvider() { @Override public Iterable loadKeys() { return Collections.singletonList(keyPair); } }); fileSystemFactory = new AndroidFileSystemFactory(context); sshd.setFileSystemFactory(fileSystemFactory); sshd.setCommandFactory(new ScpCommandFactory()); sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory())); - if (device.publicKey != null) { - keyAuth.deviceKey = device.publicKey; - } else { - keyAuth.deviceKey = device.certificate.getPublicKey(); - } + keyAuth.deviceKey = device.certificate.getPublicKey(); + sshd.setPublickeyAuthenticator(keyAuth); sshd.setPasswordAuthenticator(passwordAuth); } public boolean start(List storageInfoList) { if (!started) { fileSystemFactory.initRoots(storageInfoList); passwordAuth.password = RandomHelper.randomString(28); port = STARTPORT; while (!started) { try { sshd.setPort(port); sshd.start(); started = true; } catch (Exception e) { port++; if (port >= ENDPORT) { port = -1; Log.e("SftpServer", "No more ports available"); return false; } } } } return true; } public void stop() { try { started = false; sshd.stop(true); } catch (Exception e) { Log.e("SFTP", "Exception while stopping the server", e); } } public boolean isStarted() { return started; } String getPassword() { return passwordAuth.password; } int getPort() { return port; } String getLocalIpAddress() { String ip6 = null; try { for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { NetworkInterface intf = en.nextElement(); // Anything with rmnet is related to cellular connections or USB // tethering mechanisms. See: // // https://android.googlesource.com/kernel/msm/+/android-msm-flo-3.4-kitkat-mr1/Documentation/usb/gadget_rmnet.txt // // If we run across an interface that has this, we can safely // ignore it. In fact, it's much safer to do. If we don't, we // might get invalid IP adddresses out of it. if (intf.getDisplayName().contains("rmnet")) continue; for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { InetAddress inetAddress = enumIpAddr.nextElement(); if (!inetAddress.isLoopbackAddress()) { String address = inetAddress.getHostAddress(); if (inetAddress instanceof Inet4Address) { //Prefer IPv4 over IPv6, because sshfs doesn't seem to like IPv6 return address; } else { ip6 = address; } } } } } catch (SocketException ignored) { } return ip6; } static class SimplePasswordAuthenticator implements PasswordAuthenticator { String password; @Override public boolean authenticate(String user, String password, ServerSession session) { return user.equals(SimpleSftpServer.USER) && password.equals(this.password); } } static class SimplePublicKeyAuthenticator implements PublickeyAuthenticator { PublicKey deviceKey; @Override public boolean authenticate(String user, PublicKey key, ServerSession session) { return deviceKey.equals(key); } } } diff --git a/tests/org/kde/kdeconnect/DeviceTest.java b/tests/org/kde/kdeconnect/DeviceTest.java index 1a880a24..b0e8ef0f 100644 --- a/tests/org/kde/kdeconnect/DeviceTest.java +++ b/tests/org/kde/kdeconnect/DeviceTest.java @@ -1,284 +1,284 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect; import android.app.NotificationManager; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; 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.SecurityHelpers.RsaHelper; import org.mockito.ArgumentCaptor; 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.Method; import java.security.KeyPair; import java.security.KeyPairGenerator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.eq; @RunWith(PowerMockRunner.class) @PrepareForTest({Base64.class, Log.class, PreferenceManager.class}) public class DeviceTest { private Context context; // Creating a paired device before each test case @Before public void setUp() { // Save new test device in settings 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; } this.context = Mockito.mock(Context.class); PowerMockito.mockStatic(Base64.class); PowerMockito.when(Base64.encodeToString(any(), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeEncoder().encodeToString((byte[]) invocation.getArguments()[0])); PowerMockito.when(Base64.decode(anyString(), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeDecoder().decode((String) invocation.getArguments()[0])); PowerMockito.mockStatic(Log.class); //Store device information needed to create a Device object in a future MockSharedPreference deviceSettings = new MockSharedPreference(); 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.apply(); Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings); //Store the device as trusted MockSharedPreference trustedSettings = new MockSharedPreference(); trustedSettings.edit().putBoolean(deviceId, true).apply(); Mockito.when(context.getSharedPreferences(eq("trusted_devices"), eq(Context.MODE_PRIVATE))).thenReturn(trustedSettings); //Store an untrusted device MockSharedPreference untrustedSettings = new MockSharedPreference(); Mockito.when(context.getSharedPreferences(eq("unpairedTestDevice"), eq(Context.MODE_PRIVATE))).thenReturn(untrustedSettings); //Default shared prefs, including our own private key PowerMockito.mockStatic(PreferenceManager.class); MockSharedPreference defaultSettings = new MockSharedPreference(); PowerMockito.when(PreferenceManager.getDefaultSharedPreferences(any())).thenReturn(defaultSettings); RsaHelper.initialiseRsaKeys(context); Mockito.when(context.getSystemService(eq(Context.NOTIFICATION_SERVICE))).thenReturn(Mockito.mock(NotificationManager.class)); } @Test public void testDeviceType() { assertEquals(Device.DeviceType.Phone, Device.DeviceType.FromString(Device.DeviceType.Phone.toString())); assertEquals(Device.DeviceType.Tablet, Device.DeviceType.FromString(Device.DeviceType.Tablet.toString())); assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(Device.DeviceType.Computer.toString())); assertEquals(Device.DeviceType.Tv, Device.DeviceType.FromString(Device.DeviceType.Tv.toString())); assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString("")); assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(null)); } // Basic paired device testing @Test public void testDevice() { Device device = new Device(context, "testDevice"); assertEquals(device.getDeviceId(), "testDevice"); assertEquals(device.getDeviceType(), Device.DeviceType.Phone); assertEquals(device.getName(), "Test Device"); assertTrue(device.isPaired()); } // Testing pairing done // Created an unpaired device inside this test @Test public void testPairingDone() { NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY); fakeNetworkPacket.set("deviceId", "unpairedTestDevice"); fakeNetworkPacket.set("deviceName", "Unpaired Test Device"); fakeNetworkPacket.set("protocolVersion", NetworkPacket.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); KeyPair keyPair; try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); keyPair = keyGen.genKeyPair(); } catch (Exception e) { Log.e("KDE/initializeRsaKeys", "Exception", e); Log.e("KDE/initializeRsaKeys", "Exception", e); return; } device.publicKey = keyPair.getPublic(); ArgumentCaptor 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); assertNotNull(device.publicKey); 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() throws Exception { + public void testPairingDoneWithCertificate() { KeyPair keyPair = null; try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); keyPair = keyGen.genKeyPair(); } catch (Exception e) { Log.e("KDE/initializeRsaKeys", "Exception", e); } NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY); fakeNetworkPacket.set("deviceId", "unpairedTestDevice"); fakeNetworkPacket.set("deviceName", "Unpaired Test Device"); fakeNetworkPacket.set("protocolVersion", NetworkPacket.ProtocolVersion); fakeNetworkPacket.set("deviceType", Device.DeviceType.Phone.toString()); fakeNetworkPacket.set("certificate", "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=="); 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.publicKey = keyPair.getPublic(); assertNotNull(device); assertEquals(device.getDeviceId(), "unpairedTestDevice"); assertEquals(device.getName(), "Unpaired Test Device"); assertEquals(device.getDeviceType(), Device.DeviceType.Phone); assertNotNull(device.publicKey); 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); } assertTrue(device.isPaired()); 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 testUnpair() { Device.PairingCallback pairingCallback = Mockito.mock(Device.PairingCallback.class); Device device = new Device(context, "testDevice"); device.addPairingCallback(pairingCallback); device.unpair(); assertFalse(device.isPaired()); SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); assertFalse(preferences.getBoolean(device.getDeviceId(), false)); Mockito.verify(pairingCallback, Mockito.times(1)).unpaired(); } } diff --git a/tests/org/kde/kdeconnect/LanLinkTest.java b/tests/org/kde/kdeconnect/LanLinkTest.java index 79420c48..d51e627a 100644 --- a/tests/org/kde/kdeconnect/LanLinkTest.java +++ b/tests/org/kde/kdeconnect/LanLinkTest.java @@ -1,242 +1,244 @@ /* * Copyright 2015 Vineet Garg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect; import android.content.Context; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.kde.kdeconnect.Backends.LanBackend.LanLink; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; 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.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; +import javax.net.ssl.SSLSocket; + import static org.junit.Assert.assertEquals; @RunWith(PowerMockRunner.class) @PrepareForTest({Log.class}) public class LanLinkTest { private LanLink badLanLink; private LanLink goodLanLink; private OutputStream badOutputStream; private OutputStream goodOutputStream; private Device.SendPacketStatusCallback callback; @Before public void setUp() throws Exception { PowerMockito.mockStatic(Log.class); LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class); Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider"); callback = Mockito.mock(Device.SendPacketStatusCallback.class); goodOutputStream = Mockito.mock(OutputStream.class); badOutputStream = Mockito.mock(OutputStream.class); Mockito.doThrow(new IOException("AAA")).when(badOutputStream).write(Mockito.any(byte[].class)); - Socket socketMock = Mockito.mock(Socket.class); + SSLSocket socketMock = Mockito.mock(SSLSocket.class); Mockito.when(socketMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketMock.getOutputStream()).thenReturn(goodOutputStream); - Socket socketBadMock = Mockito.mock(Socket.class); + SSLSocket socketBadMock = Mockito.mock(SSLSocket.class); Mockito.when(socketBadMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketBadMock.getOutputStream()).thenReturn(badOutputStream); Context context = Mockito.mock(Context.class); goodLanLink = new LanLink(context, "testDevice", linkProvider, socketMock, LanLink.ConnectionStarted.Remotely); badLanLink = new LanLink(context, "testDevice", linkProvider, socketBadMock, LanLink.ConnectionStarted.Remotely); } @Test public void testSendPacketSuccess() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketSuccess"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketSuccess\"}}"); goodLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onSuccess(); } @Test public void testSendPacketFail() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketFail"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketFail\"}}"); badLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onFailure(Mockito.any(IOException.class)); } @Test public void testSendPayload() throws Exception { class Downloader extends Thread { NetworkPacket np; final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); void setNetworkPacket(NetworkPacket networkPacket) { this.np = networkPacket; } ByteArrayOutputStream getOutputStream() { return outputStream; } @Override public void run() { try { Socket socket = null; try { socket = new Socket(); int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress address = new InetSocketAddress(5000); socket.connect(new InetSocketAddress(address.getAddress(), tcpPort)); np.setPayload(new NetworkPacket.Payload(socket.getInputStream(), np.getPayloadSize())); } catch (Exception e) { socket.close(); Log.e("KDE/LanLinkTest", "Exception connecting to remote socket", e); throw e; } final InputStream input = np.getPayload().getInputStream(); final long fileLength = np.getPayloadSize(); byte data[] = new byte[1024]; long progress = 0, prevProgressPercentage = 0; int count; while ((count = input.read(data)) >= 0) { progress += count; outputStream.write(data, 0, count); if (fileLength > 0) { if (progress >= fileLength) break; long progressPercentage = (progress * 100 / fileLength); if (progressPercentage != prevProgressPercentage) { prevProgressPercentage = progressPercentage; } } } outputStream.close(); input.close(); } catch (Exception e) { Log.e("Downloader Test", "Exception", e); } } } final Downloader downloader = new Downloader(); // Using byte array for payload, try to use input stream as used in real device String dataString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + " Cras vel erat et ante fringilla tristique. Sed consequat ligula at interdum " + "rhoncus. Integer semper enim felis, id sodales tellus aliquet eget." + " Sed fringilla ac metus eget dictum. Aliquam euismod non sem sit" + " amet dapibus. Interdum et malesuada fames ac ante ipsum primis " + "in faucibus. Nam et ligula placerat, varius justo eu, convallis " + "lorem. Nam consequat consequat tortor et gravida. Praesent " + "ultricies tortor eget ex elementum gravida. Suspendisse aliquet " + "erat a orci feugiat dignissim."; // reallyLongString contains dataString 16 times String reallyLongString = dataString + dataString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; final byte[] data = reallyLongString.getBytes(); final JSONObject sharePacketJson = new JSONObject("{\"id\":123,\"body\":{\"filename\":\"data.txt\"},\"payloadTransferInfo\":{},\"payloadSize\":8720,\"type\":\"kdeconnect.share\"}"); // Mocking share package final NetworkPacket sharePacket = Mockito.mock(NetworkPacket.class); Mockito.when(sharePacket.getType()).thenReturn("kdeconnect.share"); Mockito.when(sharePacket.hasPayload()).thenReturn(true); Mockito.when(sharePacket.hasPayloadTransferInfo()).thenReturn(true); Mockito.doAnswer(invocationOnMock -> sharePacketJson.toString()).when(sharePacket).serialize(); Mockito.when(sharePacket.getPayload()).thenReturn(new NetworkPacket.Payload(new ByteArrayInputStream(data), -1)); Mockito.when(sharePacket.getPayloadSize()).thenReturn((long) data.length); Mockito.doAnswer(invocationOnMock -> sharePacketJson.getJSONObject("payloadTransferInfo")).when(sharePacket).getPayloadTransferInfo(); Mockito.doAnswer(invocationOnMock -> { JSONObject object = (JSONObject) invocationOnMock.getArguments()[0]; sharePacketJson.put("payloadTransferInfo", object); return null; }).when(sharePacket).setPayloadTransferInfo(Mockito.any(JSONObject.class)); Mockito.doAnswer(invocationOnMock -> { Log.e("LanLinkTest", "Write to stream"); String stringNetworkPacket = new String((byte[]) invocationOnMock.getArguments()[0]); final NetworkPacket np = NetworkPacket.unserialize(stringNetworkPacket); downloader.setNetworkPacket(np); downloader.start(); return stringNetworkPacket.length(); }).when(goodOutputStream).write(Mockito.any(byte[].class)); goodLanLink.sendPacket(sharePacket, callback); try { // Wait 1 secs for downloader to finish (if some error, it will continue and assert will fail) downloader.join(1000); } catch (Exception e) { Log.e("Test", "Exception", e); throw e; } assertEquals(new String(data), new String(downloader.getOutputStream().toByteArray())); Mockito.verify(callback).onSuccess(); } }