diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java deleted file mode 100644 index 48b652204..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferClientInteractor.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.Socket; -import java.net.SocketTimeoutException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import android.net.PskKeyManager; -import android.net.Uri; -import android.os.Build.VERSION_CODES; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.RequiresApi; -import android.util.Base64; - -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 KeyTransferClientInteractor { - private static final int CONNECTION_ESTABLISHED = 2; - private 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; - - Uri uri = Uri.parse(connectionDetails); - final byte[] presharedKey = Base64.decode(uri.getUserInfo(), Base64.URL_SAFE | Base64.NO_PADDING); - final String host = uri.getHost(); - final int port = uri.getPort(); - - handler = new Handler(Looper.getMainLooper()); - socketThread = new Thread() { - @Override - public void run() { - serverSocket = null; - Socket socket = null; - BufferedReader bufferedReader = null; - try { - PKM pskKeyManager = new PKM(presharedKey); - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); - socket = sslContext.getSocketFactory().createSocket(InetAddress.getByName(host), 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 { - byte[] presharedKey; - - private PKM(byte[] presharedKey) { - this.presharedKey = presharedKey; - } - - @Override - public SecretKey getKey(String identityHint, String identity, Socket socket) { - return new SecretKeySpec(presharedKey, "AES"); - } - - @Override - public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { - return new SecretKeySpec(presharedKey, "AES"); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java new file mode 100644 index 000000000..b83f0e226 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferInteractor.java @@ -0,0 +1,317 @@ +/* + * 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.network; + + +import java.io.BufferedOutputStream; +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.security.SecureRandom; +import java.util.Collections; +import java.util.List; + +import android.net.PskKeyManager; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.RequiresApi; +import android.util.Base64; + +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 KeyTransferInteractor { + private static final int SHOW_CONNECTION_DETAILS = 1; + private static final int CONNECTION_ESTABLISHED = 2; + private static final int CONNECTION_LOST = 3; + + + private TransferThread transferThread; + + + public void connectToServer(String connectionDetails, KeyTransferCallback callback) { + Uri uri = Uri.parse(connectionDetails); + final byte[] presharedKey = Base64.decode(uri.getUserInfo(), Base64.URL_SAFE | Base64.NO_PADDING); + final String host = uri.getHost(); + final int port = uri.getPort(); + + transferThread = TransferThread.createClientTransferThread(callback, presharedKey, host, port); + transferThread.start(); + } + + public void startServer(KeyTransferCallback callback) { + byte[] presharedKey = generatePresharedKey(); + + transferThread = TransferThread.createServerTransferThread(callback, presharedKey); + transferThread.start(); + } + + private static class TransferThread extends Thread { + private final Handler handler; + private final KeyTransferCallback callback; + private final byte[] presharedKey; + private final boolean isServer; + private final String clientHost; + private final Integer clientPort; + + private SSLServerSocket serverSocket; + private byte[] dataToSend; + + static TransferThread createClientTransferThread(KeyTransferCallback callback, byte[] presharedKey, + String host, int port) { + return new TransferThread(callback, presharedKey, false, host, port); + } + + static TransferThread createServerTransferThread(KeyTransferCallback callback, byte[] presharedKey) { + return new TransferThread(callback, presharedKey, true, null, null); + } + + private TransferThread(KeyTransferCallback callback, byte[] presharedKey, boolean isServer, + String clientHost, Integer clientPort) { + this.callback = callback; + this.presharedKey = presharedKey; + this.clientHost = clientHost; + this.clientPort = clientPort; + this.isServer = isServer; + + handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void run() { + SSLContext sslContext = createTlsPskSslContext(presharedKey); + + Socket socket = null; + try { + if (isServer) { + int port = 1336; + serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port); + + String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING); + String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port; + invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData); + + socket = serverSocket.accept(); + invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); + } else { + socket = sslContext.getSocketFactory().createSocket(InetAddress.getByName(clientHost), clientPort); + invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); + } + + handleOpenConnection(socket); + } catch (IOException e) { + Log.e(Constants.TAG, "error!", e); + } finally { + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + // ignore + } + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (IOException e) { + // ignore + } + } + } + + private static SSLContext createTlsPskSslContext(byte[] presharedKey) { + try { + PresharedKeyManager pskKeyManager = new PresharedKeyManager(presharedKey); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); + + return sslContext; + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + private void handleOpenConnection(Socket socket) throws IOException { + socket.setSoTimeout(500); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + while (!isInterrupted() && socket.isConnected()) { + if (dataToSend != null) { + BufferedOutputStream bufferedOutputStream = + new BufferedOutputStream(socket.getOutputStream()); + bufferedOutputStream.write(dataToSend); + bufferedOutputStream.close(); + dataToSend = null; + break; + } + try { + String line = bufferedReader.readLine(); + if (line == null) { + Log.d(Constants.TAG, "eof"); + break; + } + Log.d(Constants.TAG, "got line: " + line); + } catch (SocketTimeoutException e) { + // ignore + } + } + Log.d(Constants.TAG, "disconnected"); + invokeListener(CONNECTION_LOST, 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 SHOW_CONNECTION_DETAILS: + callback.onServerStarted(arg); + break; + case CONNECTION_ESTABLISHED: + callback.onConnectionEstablished(arg); + break; + case CONNECTION_LOST: + callback.onConnectionLost(); + } + } + }; + + handler.post(runnable); + } + + public synchronized void sendDataAndClose(byte[] dataToSend) { + this.dataToSend = dataToSend; + } + + @Override + public void interrupt() { + super.interrupt(); + + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + private static byte[] generatePresharedKey() { + byte[] presharedKey = new byte[16]; + new SecureRandom().nextBytes(presharedKey); + return presharedKey; + } + + public void closeConnection() { + if (transferThread != null) { + transferThread.interrupt(); + } + + transferThread = null; + } + + public void sendData(byte[] dataToSend) { + transferThread.sendDataAndClose(dataToSend); + } + + public interface KeyTransferCallback { + 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 PresharedKeyManager extends PskKeyManager implements KeyManager { + byte[] presharedKey; + + private PresharedKeyManager(byte[] presharedKey) { + this.presharedKey = presharedKey; + } + + @Override + public SecretKey getKey(String identityHint, String identity, Socket socket) { + return new SecretKeySpec(presharedKey, "AES"); + } + + @Override + public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { + return new SecretKeySpec(presharedKey, "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 deleted file mode 100644 index 73fb5e536..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/KeyTransferServerInteractor.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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.security.SecureRandom; -import java.util.Arrays; -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 android.util.Base64; - -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; - - byte[] presharedKey = generatePresharedKey(); - PKM pskKeyManager = new PKM(presharedKey); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(new KeyManager[] { pskKeyManager }, new TrustManager[0], null); - serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port); - - String presharedKeyEncoded = Base64.encodeToString(presharedKey, Base64.URL_SAFE | Base64.NO_PADDING); - String qrCodeData = presharedKeyEncoded + "@" + getIPAddress(true) + ":" + port; - invokeListener(SHOW_CONNECTION_DETAILS, qrCodeData); - - socket = serverSocket.accept(); - invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString()); - Arrays.fill(presharedKey, (byte) 0); - - 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 byte[] generatePresharedKey() { - byte[] presharedKey = new byte[16]; - new SecureRandom().nextBytes(presharedKey); - return presharedKey; - } - - 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() { - if (callback == null) { - return; - } - 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 { - byte[] presharedKey; - - private PKM(byte[] presharedKey) { - this.presharedKey = presharedKey; - } - - @Override - public SecretKey getKey(String identityHint, String identity, Socket socket) { - return new SecretKeySpec(presharedKey, "AES"); - } - - @Override - public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { - return new SecretKeySpec(presharedKey, "AES"); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java index 5277cd393..f2626bcf2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java @@ -13,12 +13,12 @@ import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; @@ -236,19 +236,27 @@ public class KeyRepository { } } - private String getKeyRingAsArmoredString(byte[] data) throws IOException, PgpGeneralException { - UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(data); - + private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); - keyRing.encodeArmored(bos, null); + ArmoredOutputStream aos = new ArmoredOutputStream(bos); - return bos.toString("UTF-8"); + aos.write(data); + aos.close(); + + return bos.toByteArray(); } public String getPublicKeyRingAsArmoredString(long masterKeyId) throws NotFoundException, IOException, PgpGeneralException { byte[] data = loadPublicKeyRingData(masterKeyId); - return getKeyRingAsArmoredString(data); + byte[] armoredData = getKeyRingAsArmoredData(data); + return new String(armoredData); + } + + public byte[] getSecretKeyRingAsArmoredData(long masterKeyId) + throws NotFoundException, IOException, PgpGeneralException { + byte[] data = loadSecretKeyRingData(masterKeyId); + return getKeyRingAsArmoredData(data); } public ContentResolver getContentResolver() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java new file mode 100644 index 000000000..b4b8e4824 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/loader/SecretKeyLoader.java @@ -0,0 +1,120 @@ +/* + * 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.loader; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; + + +public class SecretKeyLoader extends AsyncTaskLoader> { + public static final String[] PROJECTION = new String[] { + KeyRings.MASTER_KEY_ID, + KeyRings.CREATION, + KeyRings.NAME, + KeyRings.EMAIL, + KeyRings.HAS_ANY_SECRET + }; + private static final int INDEX_KEY_ID = 0; + private static final int INDEX_CREATION = 1; + private static final int INDEX_NAME = 2; + private static final int INDEX_EMAIL = 3; + + + private final ContentResolver contentResolver; + + private List cachedResult; + + + public SecretKeyLoader(Context context, ContentResolver contentResolver) { + super(context); + + this.contentResolver = contentResolver; + } + + @Override + public List loadInBackground() { + String where = KeyRings.HAS_ANY_SECRET + " = 1"; + Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, where, null, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading key items!"); + return null; + } + + try { + ArrayList secretKeyItems = new ArrayList<>(); + while (cursor.moveToNext()) { + SecretKeyItem secretKeyItem = new SecretKeyItem(cursor); + secretKeyItems.add(secretKeyItem); + } + + return Collections.unmodifiableList(secretKeyItems); + } finally { + cursor.close(); + } + } + + @Override + public void deliverResult(List keySubkeyStatus) { + cachedResult = keySubkeyStatus; + + if (isStarted()) { + super.deliverResult(keySubkeyStatus); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + public static class SecretKeyItem { + final int position; + public final long masterKeyId; + public final long creationMillis; + public final String name; + public final String email; + + SecretKeyItem(Cursor cursor) { + position = cursor.getPosition(); + + masterKeyId = cursor.getLong(INDEX_KEY_ID); + creationMillis = cursor.getLong(INDEX_CREATION) * 1000; + + name = cursor.getString(INDEX_NAME); + email = cursor.getString(INDEX_EMAIL); + } + } +} 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 index f211bd7ec..9e0d049de 100644 --- 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 @@ -18,33 +18,61 @@ package org.sufficientlysecure.keychain.ui.transfer.presenter; +import java.io.IOException; +import java.util.List; + import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build.VERSION_CODES; +import android.os.Bundle; import android.support.annotation.RequiresApi; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; +import android.support.v7.widget.RecyclerView.Adapter; +import android.view.LayoutInflater; -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.network.KeyTransferInteractor; +import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener; +import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; @RequiresApi(api = VERSION_CODES.LOLLIPOP) -public class TransferPresenter implements KeyTransferServerCallback, KeyTransferClientCallback { +public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>, + OnClickTransferKeyListener { private final Context context; private final TransferMvpView view; + private final LoaderManager loaderManager; + private final int loaderId; - private KeyTransferServerInteractor keyTransferServerInteractor; - private KeyTransferClientInteractor keyTransferClientInteractor; + private KeyTransferInteractor keyTransferClientInteractor; + private KeyTransferInteractor keyTransferServerInteractor; + private final TransferKeyAdapter secretKeyAdapter; - public TransferPresenter(Context context, TransferMvpView view) { + public TransferPresenter(Context context, LoaderManager loaderManager, int loaderId, TransferMvpView view) { this.context = context; this.view = view; + this.loaderManager = loaderManager; + this.loaderId = loaderId; + + secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this); + view.setSecretKeyAdapter(secretKeyAdapter); } - public void onDestroy() { + public void onStart() { + loaderManager.restartLoader(loaderId, null, this); + + startServer(); + } + + public void onStop() { clearConnections(); } @@ -55,13 +83,13 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer } public void onQrCodeScanned(String qrCodeContent) { - keyTransferClientInteractor = new KeyTransferClientInteractor(); + keyTransferClientInteractor = new KeyTransferInteractor(); keyTransferClientInteractor.connectToServer(qrCodeContent, this); } private void clearConnections() { if (keyTransferServerInteractor != null) { - keyTransferServerInteractor.stopServer(); + keyTransferServerInteractor.closeConnection(); keyTransferServerInteractor = null; } if (keyTransferClientInteractor != null) { @@ -71,7 +99,7 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer } public void startServer() { - keyTransferServerInteractor = new KeyTransferServerInteractor(); + keyTransferServerInteractor = new KeyTransferInteractor(); keyTransferServerInteractor.startServer(this); } @@ -92,12 +120,43 @@ public class TransferPresenter implements KeyTransferServerCallback, KeyTransfer startServer(); } - public interface TransferMvpView { - void showConnectionEstablished(String hostname); - void showWaitingForConnection(); + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return secretKeyAdapter.createLoader(context); + } - void setQrImage(Bitmap qrCode); + @Override + public void onLoadFinished(Loader> loader, List data) { + secretKeyAdapter.setData(data); + } + + @Override + public void onLoaderReset(Loader> loader) { + secretKeyAdapter.setData(null); + } + + @Override + public void onClickTransferKey(long masterKeyId) { + try { + byte[] armoredSecretKey = + KeyRepository.createDatabaseInteractor(context).getSecretKeyRingAsArmoredData(masterKeyId); + if (keyTransferClientInteractor != null) { + keyTransferClientInteractor.sendData(armoredSecretKey); + } else if (keyTransferServerInteractor != null) { + keyTransferServerInteractor.sendData(armoredSecretKey); + } + } catch (IOException | NotFoundException | PgpGeneralException e) { + e.printStackTrace(); + } + } + + public interface TransferMvpView { + void showWaitingForConnection(); + void showConnectionEstablished(String hostname); void scanQrCode(); + void setQrImage(Bitmap qrCode); + + void setSecretKeyAdapter(Adapter adapter); } } 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 index 322df19a8..a8cf8f808 100644 --- 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 @@ -26,6 +26,8 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.app.Fragment; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -47,12 +49,14 @@ public class TransferFragment extends Fragment implements TransferMvpView { public static final int VIEW_WAITING = 0; public static final int VIEW_CONNECTED = 1; public static final int REQUEST_CODE_SCAN = 1; + public static final int LOADER_ID = 1; private ImageView vQrCodeImage; private TransferPresenter presenter; private ViewAnimator vTransferAnimator; private TextView vConnectionStatusText; + private RecyclerView vTransferKeyList; @Override @@ -62,6 +66,7 @@ public class TransferFragment extends Fragment implements TransferMvpView { vTransferAnimator = (ViewAnimator) view.findViewById(R.id.transfer_animator); vConnectionStatusText = (TextView) view.findViewById(R.id.connection_status); + vTransferKeyList = (RecyclerView) view.findViewById(R.id.transfer_key_list); vQrCodeImage = (ImageView) view.findViewById(R.id.qr_code_image); @@ -82,22 +87,21 @@ public class TransferFragment extends Fragment implements TransferMvpView { public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - presenter = new TransferPresenter(getContext(), this); + presenter = new TransferPresenter(getContext(), getLoaderManager(), LOADER_ID, this); } @Override public void onStart() { super.onStart(); - presenter.startServer(); + presenter.onStart(); } - @Override public void onStop() { super.onStop(); - presenter.onDestroy(); + presenter.onStop(); } @Override @@ -132,6 +136,11 @@ public class TransferFragment extends Fragment implements TransferMvpView { startActivityForResult(intent, REQUEST_CODE_SCAN); } + @Override + public void setSecretKeyAdapter(Adapter adapter) { + vTransferKeyList.setAdapter(adapter); + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java new file mode 100644 index 000000000..46b86be67 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java @@ -0,0 +1,144 @@ +package org.sufficientlysecure.keychain.ui.transfer.view; + + +import java.util.List; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v4.content.Loader; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader; +import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; + + +public class TransferSecretKeyList extends RecyclerView { + private OnClickTransferKeyListener onClickTransferKeyListener; + + public TransferSecretKeyList(Context context) { + super(context); + init(context); + } + + public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public TransferSecretKeyList(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setLayoutManager(new LinearLayoutManager(context)); + addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST)); + } + + public static class TransferKeyAdapter extends RecyclerView.Adapter { + private final Context context; + private final LayoutInflater layoutInflater; + private final OnClickTransferKeyListener onClickTransferKeyListener; + + private List data; + + + public TransferKeyAdapter(Context context, LayoutInflater layoutInflater, + OnClickTransferKeyListener onClickTransferKeyListener) { + this.context = context; + this.layoutInflater = layoutInflater; + this.onClickTransferKeyListener = onClickTransferKeyListener; + } + + @Override + public TransferKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false)); + } + + @Override + public void onBindViewHolder(TransferKeyViewHolder holder, int position) { + SecretKeyItem item = data.get(position); + holder.bind(context, item, onClickTransferKeyListener); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + @Override + public long getItemId(int position) { + return data.get(position).masterKeyId; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + public Loader> createLoader(Context context) { + return new SecretKeyLoader(context, context.getContentResolver()); + } + } + + static class TransferKeyViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vEmail; + private final TextView vCreation; + private final View vSendButton; + + public TransferKeyViewHolder(View itemView) { + super(itemView); + + vName = (TextView) itemView.findViewById(R.id.key_list_item_name); + vEmail = (TextView) itemView.findViewById(R.id.key_list_item_email); + vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + + vSendButton = itemView.findViewById(R.id.button_transfer); + } + + private void bind(Context context, final SecretKeyItem item, final OnClickTransferKeyListener onClickTransferKeyListener) { + if (item.name != null) { + vName.setText(item.name); + vName.setVisibility(View.VISIBLE); + } else { + vName.setVisibility(View.GONE); + } + if (item.email != null) { + vEmail.setText(item.email); + vEmail.setVisibility(View.VISIBLE); + } else { + vEmail.setVisibility(View.GONE); + } + + String dateTime = DateUtils.formatDateTime(context, item.creationMillis, + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + if (onClickTransferKeyListener != null) { + vSendButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onClickTransferKeyListener.onClickTransferKey(item.masterKeyId); + } + }); + } else { + vSendButton.setOnClickListener(null); + } + } + } + + public interface OnClickTransferKeyListener { + void onClickTransferKey(long masterKeyId); + } +} diff --git a/OpenKeychain/src/main/res/layout/key_transfer_item.xml b/OpenKeychain/src/main/res/layout/key_transfer_item.xml new file mode 100644 index 000000000..9271f66a2 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_transfer_item.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/transfer_fragment.xml b/OpenKeychain/src/main/res/layout/transfer_fragment.xml index b99369751..6e19a1373 100644 --- a/OpenKeychain/src/main/res/layout/transfer_fragment.xml +++ b/OpenKeychain/src/main/res/layout/transfer_fragment.xml @@ -55,27 +55,33 @@ + android:orientation="vertical"> + android:textAppearance="?android:attr/textAppearanceMedium" + style="@style/SectionHeader" + /> + +