diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java new file mode 100644 index 000000000..69bf29e0b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2017 Tobias Schülke + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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.sufficientlysecure.keychain.network; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; + +import android.net.PskKeyManager; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.RequiresApi; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class KeyTransferClientInteractor { + private static final int SHOW_CONNECTION_DETAILS = 1; + private static final int CONNECTION_ESTABLISHED = 2; + public static final int CONNECTION_LOST = 3; + + + private Thread socketThread; + private KeyTransferClientCallback callback; + private Handler handler; + private SSLServerSocket serverSocket; + + + public void connectToServer(final String connectionDetails, KeyTransferClientCallback callback) { + this.callback = callback; + + handler = new Handler(Looper.getMainLooper()); + socketThread = new Thread() { + @Override + public void run() { + serverSocket = null; + Socket socket = null; + BufferedReader bufferedReader = null; + try { + int port = 1336; + + PKM pskKeyManager = new PKM(); + SSLContext sslContext = null; + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); + socket = sslContext.getSocketFactory().createSocket(InetAddress.getByName(connectionDetails), port); + + invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); + + socket.setSoTimeout(500); + bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + while (!isInterrupted() && socket.isConnected()) { + try { + String line = bufferedReader.readLine(); + if (line == null) { + break; + } + Log.d(Constants.TAG, "got line: " + line); + } catch (SocketTimeoutException e) { + // ignore + } + } + Log.d(Constants.TAG, "disconnected"); + invokeListener(CONNECTION_LOST, null); + } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) { + Log.e(Constants.TAG, "error!", e); + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + // ignore + } + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + // ignore + } + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (IOException e) { + // ignore + } + } + } + }; + + socketThread.start(); + } + + public void closeConnection() { + if (socketThread != null) { + socketThread.interrupt(); + } + + socketThread = null; + callback = null; + } + + private void invokeListener(final int method, final String arg) { + if (handler == null) { + return; + } + + Runnable runnable = new Runnable() { + @Override + public void run() { + if (callback == null) { + return; + } + + switch (method) { + case CONNECTION_ESTABLISHED: + callback.onConnectionEstablished(arg); + break; + case CONNECTION_LOST: + callback.onConnectionLost(); + } + } + }; + + handler.post(runnable); + } + + public interface KeyTransferClientCallback { + void onConnectionEstablished(String otherName); + void onConnectionLost(); + } + + private static class PKM extends PskKeyManager implements KeyManager { + @Override + public SecretKey getKey(String identityHint, String identity, Socket socket) { + return new SecretKeySpec("swag".getBytes(), "AES"); + } + + @Override + public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { + return new SecretKeySpec("swag".getBytes(), "AES"); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java new file mode 100644 index 000000000..e0d27ffff --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 Tobias Schülke + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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.sufficientlysecure.keychain.network; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; + +import android.net.PskKeyManager; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.RequiresApi; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.TrustManager; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class KeyTransferServerInteractor { + private static final int SHOW_CONNECTION_DETAILS = 1; + private static final int CONNECTION_ESTABLISHED = 2; + public static final int CONNECTION_LOST = 3; + + + private Thread socketThread; + private KeyTransferServerCallback callback; + private Handler handler; + private SSLServerSocket serverSocket; + + public void startServer(KeyTransferServerCallback callback) { + this.callback = callback; + + handler = new Handler(Looper.getMainLooper()); + socketThread = new Thread() { + @Override + public void run() { + serverSocket = null; + Socket socket = null; + BufferedReader bufferedReader = null; + try { + int port = 1336; + + PKM pskKeyManager = new PKM(); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); + serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port); + + String qrCodeData = getIPAddress(true) + ":" + port + ":" + "swag"; + invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData); + + socket = serverSocket.accept(); + invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); + + socket.setSoTimeout(500); + bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + while (!isInterrupted() && socket.isConnected()) { + try { + String line = bufferedReader.readLine(); + if (line == null) { + break; + } + Log.d(Constants.TAG, "got line: " + line); + } catch (SocketTimeoutException e) { + // ignore + } + } + Log.d(Constants.TAG, "disconnected"); + invokeListener(CONNECTION_LOST, null); + } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) { + Log.e(Constants.TAG, "error!", e); + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + // ignore + } + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + // ignore + } + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (IOException e) { + // ignore + } + } + } + }; + + socketThread.start(); + } + + public void stopServer() { + if (socketThread != null) { + socketThread.interrupt(); + } + + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + // ignore + } + } + + socketThread = null; + serverSocket = null; + callback = null; + } + + private void invokeListener(final int method, final String arg) { + if (handler == null) { + return; + } + + Runnable runnable = new Runnable() { + @Override + public void run() { + switch (method) { + case SHOW_CONNECTION_DETAILS: + callback.onServerStarted(arg); + break; + case CONNECTION_ESTABLISHED: + callback.onConnectionEstablished(arg); + break; + case CONNECTION_LOST: + callback.onConnectionLost(); + } + } + }; + + handler.post(runnable); + } + + public interface KeyTransferServerCallback { + void onServerStarted(String qrCodeData); + void onConnectionEstablished(String otherName); + void onConnectionLost(); + } + + /** + * from: http://stackoverflow.com/a/13007325 + *

+ * Get IP address from first non-localhost interface + * + * @param useIPv4 true=return ipv4, false=return ipv6 + * @return address or empty string + */ + private static String getIPAddress(boolean useIPv4) { + try { + List interfaces = Collections.list(NetworkInterface. + getNetworkInterfaces()); + for (NetworkInterface intf : interfaces) { + List addrs = Collections.list(intf.getInetAddresses()); + for (InetAddress addr : addrs) { + if (!addr.isLoopbackAddress()) { + String sAddr = addr.getHostAddress(); + boolean isIPv4 = sAddr.indexOf(':') < 0; + + if (useIPv4) { + if (isIPv4) + return sAddr; + } else { + if (!isIPv4) { + int delim = sAddr.indexOf('%'); // drop ip6 zone suffix + return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim). + toUpperCase(); + } + } + } + } + } + } catch (Exception ex) { + } // for now eat exceptions + return ""; + } + + private static class PKM extends PskKeyManager implements KeyManager { + @Override + public SecretKey getKey(String identityHint, String identity, Socket socket) { + return new SecretKeySpec("swag".getBytes(), "AES"); + } + + @Override + public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { + return new SecretKeySpec("swag".getBytes(), "AES"); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 13df0b539..a34d470bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -41,6 +41,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.remote.ui.AppsListFragment; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; @@ -50,8 +51,9 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai static final int ID_ENCRYPT_DECRYPT = 2; static final int ID_APPS = 3; static final int ID_BACKUP = 4; - static final int ID_SETTINGS = 5; - static final int ID_HELP = 6; + static final int ID_TRANSFER = 5; + static final int ID_SETTINGS = 6; + static final int ID_HELP = 7; // both of these are used for instrumentation testing only public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time"; @@ -82,6 +84,8 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai .withIdentifier(ID_APPS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore) .withIdentifier(ID_BACKUP).withSelectable(false), + new PrimaryDrawerItem().withName(R.string.nav_transfer).withIcon(CommunityMaterial.Icon.cmd_backup_restore) + .withIdentifier(ID_TRANSFER).withSelectable(false), new DividerDrawerItem(), new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false) @@ -105,6 +109,9 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai case ID_BACKUP: onBackupSelected(); break; + case ID_TRANSFER: + onTransferSelected(); + break; case ID_SETTINGS: intent = new Intent(MainActivity.this, SettingsActivity.class); break; @@ -207,6 +214,13 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai setFragment(frag, true); } + private void onTransferSelected() { + mToolbar.setTitle(R.string.nav_transfer); + mDrawer.setSelection(ID_TRANSFER, false); + Fragment frag = new TransferFragment(); + setFragment(frag, true); + } + @Override protected void onSaveInstanceState(Bundle outState) { // add the values which need to be saved from the drawer to the bundle diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java new file mode 100644 index 000000000..b9d981c0a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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.sufficientlysecure.keychain.ui.transfer.presenter; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.support.annotation.RequiresApi; + +import org.sufficientlysecure.keychain.network.KeyTransferClientInteractor; +import org.sufficientlysecure.keychain.network.KeyTransferClientInteractor.KeyTransferClientCallback; +import org.sufficientlysecure.keychain.network.KeyTransferServerInteractor; +import org.sufficientlysecure.keychain.network.KeyTransferServerInteractor.KeyTransferServerCallback; +import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class TransferPresenter implements KeyTransferServerCallback, KeyTransferClientCallback { + private final Context context; + private final TransferMvpView view; + + private KeyTransferServerInteractor keyTransferServerInteractor; + private KeyTransferClientInteractor keyTransferClientInteractor; + + public TransferPresenter(Context context, TransferMvpView view) { + this.context = context; + this.view = view; + } + + public void onDestroy() { + clearConnections(); + } + + public void onClickScan() { + clearConnections(); + + keyTransferClientInteractor = new KeyTransferClientInteractor(); + keyTransferClientInteractor.connectToServer("10.100.40.126", this); + } + + private void clearConnections() { + if (keyTransferServerInteractor != null) { + keyTransferServerInteractor.stopServer(); + keyTransferServerInteractor = null; + } + if (keyTransferClientInteractor != null) { + keyTransferClientInteractor.closeConnection(); + keyTransferClientInteractor = null; + } + } + + public void startServer() { + keyTransferServerInteractor = new KeyTransferServerInteractor(); + keyTransferServerInteractor.startServer(this); + } + + @Override + public void onServerStarted(String qrCodeData) { + Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse("pgp+transfer:" + qrCodeData), 0); + view.setQrImage(qrCodeBitmap); + } + + @Override + public void onConnectionEstablished(String otherName) { + view.showConnectionEstablished(otherName); + } + + @Override + public void onConnectionLost() { + view.showWaitingForConnection(); + startServer(); + } + + public interface TransferMvpView { + void showConnectionEstablished(String hostname); + void showWaitingForConnection(); + + void setQrImage(Bitmap qrCode); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java new file mode 100644 index 000000000..874eefd98 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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.sufficientlysecure.keychain.ui.transfer.view; + + +import android.graphics.Bitmap; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter; +import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView; + + +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class TransferFragment extends Fragment implements TransferMvpView { + public static final int VIEW_WAITING = 0; + public static final int VIEW_CONNECTED = 1; + + + private ImageView vQrCodeImage; + private View vScanButton; + private TransferPresenter presenter; + private ViewAnimator vTransferAnimator; + private TextView vConnectionStatusText; + + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.transfer_fragment, container, false); + + vTransferAnimator = (ViewAnimator) view.findViewById(R.id.transfer_animator); + + vConnectionStatusText = (TextView) view.findViewById(R.id.connection_status); + + vQrCodeImage = (ImageView) view.findViewById(R.id.qr_code_image); + vScanButton = view.findViewById(R.id.button_scan); + vScanButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (presenter != null) { + presenter.onClickScan(); + } + } + }); + + return view; + } + + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter = new TransferPresenter(getContext(), this); + } + + @Override + public void onStart() { + super.onStart(); + + presenter.startServer(); + } + + + @Override + public void onStop() { + super.onStop(); + + presenter.onDestroy(); + } + + @Override + public void showWaitingForConnection() { + vTransferAnimator.setDisplayedChild(VIEW_WAITING); + } + + @Override + public void showConnectionEstablished(String hostname) { + vTransferAnimator.setDisplayedChild(VIEW_CONNECTED); + vConnectionStatusText.setText("Connected to: " + hostname); + } + + @Override + public void setQrImage(final Bitmap qrCode) { + vQrCodeImage.getViewTreeObserver().addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + // create actual bitmap in display dimensions + Bitmap scaled = Bitmap.createScaledBitmap(qrCode, + vQrCodeImage.getWidth(), vQrCodeImage.getWidth(), false); + vQrCodeImage.setImageBitmap(scaled); + } + }); + vQrCodeImage.requestLayout(); + } +} diff --git a/OpenKeychain/src/main/res/layout/transfer_fragment.xml b/OpenKeychain/src/main/res/layout/transfer_fragment.xml new file mode 100644 index 000000000..b99369751 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/transfer_fragment.xml @@ -0,0 +1,84 @@ + + + + + + + + + +