drop broken secure wifi transfer feature
This feature depended on the unsupported TLS-PSK implementation shipped with Android's conscrypt implementation. It abused a duck typing mechanism that allowed using TLS-PSK despite its unsupported status, but this silently broke somewhere along the way.
This commit is contained in:
@@ -1,443 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.network;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.NoRouteToHostException;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
|
||||||
import javax.net.ssl.SSLServerSocket;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
|
|
||||||
public class KeyTransferInteractor {
|
|
||||||
private static final String[] ALLOWED_CIPHERSUITES = new String[] {
|
|
||||||
// only allow ephemeral diffie-hellman based PSK ciphers!
|
|
||||||
"TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
|
|
||||||
"TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
|
|
||||||
"TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
|
|
||||||
"TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final int CONNECTION_LISTENING = 1;
|
|
||||||
private static final int CONNECTION_ESTABLISHED = 2;
|
|
||||||
private static final int CONNECTION_SEND_OK = 3;
|
|
||||||
private static final int CONNECTION_RECEIVE_OK = 4;
|
|
||||||
private static final int CONNECTION_LOST = 5;
|
|
||||||
private static final int CONNECTION_ERROR_NO_ROUTE_TO_HOST = 6;
|
|
||||||
private static final int CONNECTION_ERROR_CONNECT = 7;
|
|
||||||
private static final int CONNECTION_ERROR_WHILE_CONNECTED = 8;
|
|
||||||
private static final int CONNECTION_ERROR_LISTEN = 0;
|
|
||||||
|
|
||||||
private static final int TIMEOUT_CONNECTING = 1500;
|
|
||||||
private static final int TIMEOUT_RECEIVING = 2000;
|
|
||||||
private static final int TIMEOUT_WAITING = 500;
|
|
||||||
private static final int PSK_BYTE_LENGTH = 16;
|
|
||||||
|
|
||||||
|
|
||||||
private final String delimiterStart;
|
|
||||||
private final String delimiterEnd;
|
|
||||||
|
|
||||||
private TransferThread transferThread;
|
|
||||||
|
|
||||||
|
|
||||||
public KeyTransferInteractor(String delimiterStart, String delimiterEnd) {
|
|
||||||
this.delimiterStart = delimiterStart;
|
|
||||||
this.delimiterEnd = delimiterEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connectToServer(String qrCodeContent, KeyTransferCallback callback) throws URISyntaxException {
|
|
||||||
SktUri sktUri = SktUri.parse(qrCodeContent);
|
|
||||||
|
|
||||||
transferThread = TransferThread.createClientTransferThread(delimiterStart, delimiterEnd, callback,
|
|
||||||
sktUri.getPresharedKey(), sktUri.getHost(), sktUri.getPort(), sktUri.getWifiSsid());
|
|
||||||
transferThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startServer(KeyTransferCallback callback, String wifiSsid) {
|
|
||||||
byte[] presharedKey = generatePresharedKey();
|
|
||||||
|
|
||||||
transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, wifiSsid);
|
|
||||||
transferThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TransferThread extends Thread {
|
|
||||||
private final String delimiterStart;
|
|
||||||
private final String delimiterEnd;
|
|
||||||
|
|
||||||
private final Handler handler;
|
|
||||||
private final byte[] presharedKey;
|
|
||||||
private final boolean isServer;
|
|
||||||
private final String clientHost;
|
|
||||||
private final Integer clientPort;
|
|
||||||
private final String wifiSsid;
|
|
||||||
|
|
||||||
private KeyTransferCallback callback;
|
|
||||||
private SSLServerSocket serverSocket;
|
|
||||||
private byte[] dataToSend;
|
|
||||||
private String sendPassthrough;
|
|
||||||
|
|
||||||
static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd,
|
|
||||||
KeyTransferCallback callback, byte[] presharedKey, String host, int port, String wifiSsid) {
|
|
||||||
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port, wifiSsid);
|
|
||||||
}
|
|
||||||
|
|
||||||
static TransferThread createServerTransferThread(String delimiterStart, String delimiterEnd,
|
|
||||||
KeyTransferCallback callback, byte[] presharedKey, String wifiSsid) {
|
|
||||||
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null, wifiSsid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TransferThread(String delimiterStart, String delimiterEnd,
|
|
||||||
KeyTransferCallback callback, byte[] presharedKey, boolean isServer,
|
|
||||||
String clientHost, Integer clientPort, String wifiSsid) {
|
|
||||||
super("TLS-PSK Key Transfer Thread");
|
|
||||||
|
|
||||||
this.delimiterStart = delimiterStart;
|
|
||||||
this.delimiterEnd = delimiterEnd;
|
|
||||||
|
|
||||||
this.callback = callback;
|
|
||||||
this.presharedKey = presharedKey;
|
|
||||||
this.clientHost = clientHost;
|
|
||||||
this.clientPort = clientPort;
|
|
||||||
this.wifiSsid = wifiSsid;
|
|
||||||
this.isServer = isServer;
|
|
||||||
|
|
||||||
handler = new Handler(Looper.getMainLooper());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SSLContext sslContext = TlsPskCompat.createTlsPskSslContext(presharedKey);
|
|
||||||
|
|
||||||
Socket socket = null;
|
|
||||||
try {
|
|
||||||
socket = getSocketListenOrConnect(sslContext);
|
|
||||||
if (socket == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handleOpenConnection(socket);
|
|
||||||
Timber.d("connection closed ok!");
|
|
||||||
} catch (SSLHandshakeException e) {
|
|
||||||
Timber.d(e, "ssl handshake error!");
|
|
||||||
invokeListener(CONNECTION_ERROR_CONNECT, null);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "communication error!");
|
|
||||||
invokeListener(CONNECTION_ERROR_WHILE_CONNECTED, e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
closeQuietly(socket);
|
|
||||||
closeQuietly(serverSocket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Socket getSocketListenOrConnect(SSLContext sslContext) {
|
|
||||||
Socket socket;
|
|
||||||
if (isServer) {
|
|
||||||
try {
|
|
||||||
serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(0);
|
|
||||||
String[] supportedCipherSuites = serverSocket.getSupportedCipherSuites();
|
|
||||||
String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
|
|
||||||
serverSocket.setEnabledCipherSuites(enabledCipherSuites);
|
|
||||||
|
|
||||||
SktUri sktUri = SktUri.create(getIPAddress(true), serverSocket.getLocalPort(), presharedKey, wifiSsid);
|
|
||||||
invokeListener(CONNECTION_LISTENING, sktUri.toUriString());
|
|
||||||
|
|
||||||
socket = serverSocket.accept();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "error while listening!");
|
|
||||||
invokeListener(CONNECTION_ERROR_LISTEN, null);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket();
|
|
||||||
String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites();
|
|
||||||
String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
|
|
||||||
sslSocket.setEnabledCipherSuites(enabledCipherSuites);
|
|
||||||
|
|
||||||
socket = sslSocket;
|
|
||||||
socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "error while connecting!");
|
|
||||||
if (e instanceof NoRouteToHostException) {
|
|
||||||
invokeListener(CONNECTION_ERROR_NO_ROUTE_TO_HOST, wifiSsid);
|
|
||||||
} else {
|
|
||||||
invokeListener(CONNECTION_ERROR_CONNECT, null);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleOpenConnection(Socket socket) throws IOException {
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
|
||||||
OutputStream outputStream = new BufferedOutputStream(socket.getOutputStream());
|
|
||||||
|
|
||||||
invokeListener(CONNECTION_ESTABLISHED, socket.getInetAddress().toString());
|
|
||||||
|
|
||||||
socket.setSoTimeout(TIMEOUT_WAITING);
|
|
||||||
while (!isInterrupted() && socket.isConnected() && !socket.isClosed()) {
|
|
||||||
sendDataIfAvailable(socket, outputStream);
|
|
||||||
boolean connectionTerminated = receiveDataIfAvailable(socket, bufferedReader);
|
|
||||||
if (connectionTerminated) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.d("disconnected");
|
|
||||||
invokeListener(CONNECTION_LOST, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean receiveDataIfAvailable(Socket socket, BufferedReader bufferedReader) throws IOException {
|
|
||||||
String firstLine;
|
|
||||||
try {
|
|
||||||
firstLine = bufferedReader.readLine();
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstLine == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean lineIsDelimiter = delimiterStart.equals(firstLine);
|
|
||||||
if (!lineIsDelimiter) {
|
|
||||||
Timber.d("bad beginning of key block?");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.setSoTimeout(TIMEOUT_RECEIVING);
|
|
||||||
String receivedData = receiveLinesUntilEndDelimiter(bufferedReader, firstLine);
|
|
||||||
socket.setSoTimeout(TIMEOUT_WAITING);
|
|
||||||
|
|
||||||
invokeListener(CONNECTION_RECEIVE_OK, receivedData);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean sendDataIfAvailable(Socket socket, OutputStream outputStream) throws IOException {
|
|
||||||
if (dataToSend != null) {
|
|
||||||
byte[] data = dataToSend;
|
|
||||||
dataToSend = null;
|
|
||||||
|
|
||||||
socket.setSoTimeout(TIMEOUT_RECEIVING);
|
|
||||||
outputStream.write(data);
|
|
||||||
outputStream.flush();
|
|
||||||
socket.setSoTimeout(TIMEOUT_WAITING);
|
|
||||||
|
|
||||||
invokeListener(CONNECTION_SEND_OK, sendPassthrough);
|
|
||||||
sendPassthrough = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String receiveLinesUntilEndDelimiter(BufferedReader bufferedReader, String line) throws IOException {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
do {
|
|
||||||
boolean lineIsDelimiter = delimiterEnd.equals(line);
|
|
||||||
if (lineIsDelimiter) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.append(line).append('\n');
|
|
||||||
|
|
||||||
line = bufferedReader.readLine();
|
|
||||||
} while (line != null);
|
|
||||||
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
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_LISTENING:
|
|
||||||
callback.onServerStarted(arg);
|
|
||||||
break;
|
|
||||||
case CONNECTION_ESTABLISHED:
|
|
||||||
callback.onConnectionEstablished(arg);
|
|
||||||
break;
|
|
||||||
case CONNECTION_RECEIVE_OK:
|
|
||||||
callback.onDataReceivedOk(arg);
|
|
||||||
break;
|
|
||||||
case CONNECTION_SEND_OK:
|
|
||||||
callback.onDataSentOk(arg);
|
|
||||||
break;
|
|
||||||
case CONNECTION_LOST:
|
|
||||||
callback.onConnectionLost();
|
|
||||||
break;
|
|
||||||
case CONNECTION_ERROR_WHILE_CONNECTED:
|
|
||||||
callback.onConnectionError(arg);
|
|
||||||
break;
|
|
||||||
case CONNECTION_ERROR_NO_ROUTE_TO_HOST:
|
|
||||||
callback.onConnectionErrorNoRouteToHost(wifiSsid);
|
|
||||||
break;
|
|
||||||
case CONNECTION_ERROR_CONNECT:
|
|
||||||
callback.onConnectionErrorConnect();
|
|
||||||
break;
|
|
||||||
case CONNECTION_ERROR_LISTEN:
|
|
||||||
callback.onConnectionErrorListen();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handler.post(runnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void sendData(byte[] dataToSend, String passthrough) {
|
|
||||||
this.dataToSend = dataToSend;
|
|
||||||
this.sendPassthrough = passthrough;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void interrupt() {
|
|
||||||
callback = null;
|
|
||||||
super.interrupt();
|
|
||||||
closeQuietly(serverSocket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] generatePresharedKey() {
|
|
||||||
byte[] presharedKey = new byte[PSK_BYTE_LENGTH];
|
|
||||||
new SecureRandom().nextBytes(presharedKey);
|
|
||||||
return presharedKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeConnection() {
|
|
||||||
if (transferThread != null) {
|
|
||||||
transferThread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
transferThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendData(byte[] dataToSend, String passthrough) {
|
|
||||||
transferThread.sendData(dataToSend, passthrough);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface KeyTransferCallback {
|
|
||||||
void onServerStarted(String qrCodeData);
|
|
||||||
void onConnectionEstablished(String otherName);
|
|
||||||
void onConnectionLost();
|
|
||||||
|
|
||||||
void onDataReceivedOk(String receivedData);
|
|
||||||
void onDataSentOk(String passthrough);
|
|
||||||
|
|
||||||
void onConnectionErrorConnect();
|
|
||||||
void onConnectionErrorNoRouteToHost(String wifiSsid);
|
|
||||||
void onConnectionErrorListen();
|
|
||||||
void onConnectionError(String arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* from: http://stackoverflow.com/a/13007325
|
|
||||||
* <p>
|
|
||||||
* 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<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
|
||||||
for (NetworkInterface intf : interfaces) {
|
|
||||||
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
|
|
||||||
for (InetAddress addr : addrs) {
|
|
||||||
if (addr.isLoopbackAddress()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String sAddr = addr.getHostAddress();
|
|
||||||
boolean isIPv4 = sAddr.indexOf(':') < 0;
|
|
||||||
if (useIPv4) {
|
|
||||||
if (isIPv4) {
|
|
||||||
return sAddr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
int delimIndex = sAddr.indexOf('%'); // drop ip6 zone suffix
|
|
||||||
if (delimIndex >= 0) {
|
|
||||||
sAddr = sAddr.substring(0, delimIndex);
|
|
||||||
}
|
|
||||||
return sAddr.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeQuietly(Closeable closeable) {
|
|
||||||
try {
|
|
||||||
if (closeable != null) {
|
|
||||||
closeable.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] intersectArrays(String[] array1, String[] array2) {
|
|
||||||
Set<String> s1 = new HashSet<>(Arrays.asList(array1));
|
|
||||||
Set<String> s2 = new HashSet<>(Arrays.asList(array2));
|
|
||||||
s1.retainAll(s2);
|
|
||||||
|
|
||||||
return s1.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.network;
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.auto.value.AutoValue;
|
|
||||||
import org.bouncycastle.util.encoders.DecoderException;
|
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
|
|
||||||
@AutoValue
|
|
||||||
abstract class SktUri {
|
|
||||||
private static final String QRCODE_URI_FORMAT = Constants.SKT_SCHEME + ":%s/%d/%s";
|
|
||||||
private static final String QRCODE_URI_FORMAT_SSID = Constants.SKT_SCHEME + ":%s/%d/%s/SSID:%s";
|
|
||||||
|
|
||||||
|
|
||||||
public abstract String getHost();
|
|
||||||
public abstract int getPort();
|
|
||||||
@SuppressWarnings("mutable")
|
|
||||||
public abstract byte[] getPresharedKey();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public abstract String getWifiSsid();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static SktUri parse(String input) throws URISyntaxException {
|
|
||||||
if (!input.startsWith(Constants.SKT_SCHEME + ":")) {
|
|
||||||
throw new URISyntaxException(input, "invalid scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] pieces = input.substring(input.indexOf(":") +1).split("/");
|
|
||||||
if (pieces.length < 3) {
|
|
||||||
throw new URISyntaxException(input, "invalid syntax");
|
|
||||||
}
|
|
||||||
|
|
||||||
String address = pieces[0];
|
|
||||||
int port;
|
|
||||||
try {
|
|
||||||
port = Integer.parseInt(pieces[1]);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new URISyntaxException(input, "error parsing port");
|
|
||||||
}
|
|
||||||
byte[] psk;
|
|
||||||
try {
|
|
||||||
psk = Hex.decode(pieces[2]);
|
|
||||||
} catch (DecoderException e) {
|
|
||||||
throw new URISyntaxException(input, "error parsing hex psk");
|
|
||||||
}
|
|
||||||
|
|
||||||
String wifiSsid = null;
|
|
||||||
for (int i = 3; i < pieces.length; i++) {
|
|
||||||
String[] optarg = pieces[i].split(":", 2);
|
|
||||||
if (optarg.length == 2 && "SSID".equals(optarg[0])) {
|
|
||||||
try {
|
|
||||||
wifiSsid = new String(Hex.decode(optarg[1]));
|
|
||||||
} catch (DecoderException e) {
|
|
||||||
Timber.d("error parsing ssid in skt uri, ignoring: " + input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AutoValue_SktUri(address, port, psk, wifiSsid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
String toUriString() {
|
|
||||||
String sktHex = Hex.toHexString(getPresharedKey());
|
|
||||||
String wifiSsid = getWifiSsid();
|
|
||||||
|
|
||||||
String result;
|
|
||||||
if (wifiSsid != null) {
|
|
||||||
String encodedWifiSsid = Hex.toHexString(getWifiSsid().getBytes(Charset.defaultCharset()));
|
|
||||||
result = String.format(QRCODE_URI_FORMAT_SSID, getHost(), getPort(), sktHex, encodedWifiSsid);
|
|
||||||
} else {
|
|
||||||
result = String.format(QRCODE_URI_FORMAT, getHost(), getPort(), sktHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
static SktUri create(String host, int port, byte[] presharedKey, @Nullable String wifiSsid) {
|
|
||||||
return new AutoValue_SktUri(host, port, presharedKey, wifiSsid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.network;
|
|
||||||
|
|
||||||
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import androidx.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.TrustManager;
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
|
|
||||||
class TlsPskCompat {
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
/* This class is a KeyManager that is compatible to TlsPskManager.
|
|
||||||
*
|
|
||||||
* Due to the way conscrypt works internally, this class will be internally duck typed to
|
|
||||||
* PSKKeyManager. This is quite a hack, and relies on conscrypt internals to work - but it
|
|
||||||
* works.
|
|
||||||
*
|
|
||||||
* see also:
|
|
||||||
* https://github.com/google/conscrypt/blob/b23e9353ed4e3256379d660cb09491a69b21affb/common/src/main/java/org/conscrypt/SSLParametersImpl.java#L494
|
|
||||||
* https://github.com/google/conscrypt/blob/29916ef38dc9cb4e4c6e3fdb87d4e921546d3ef4/common/src/main/java/org/conscrypt/DuckTypedPSKKeyManager.java#L51
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private static class PresharedKeyManager implements KeyManager {
|
|
||||||
byte[] presharedKey;
|
|
||||||
|
|
||||||
private PresharedKeyManager(byte[] presharedKey) {
|
|
||||||
this.presharedKey = presharedKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String chooseServerKeyIdentityHint(Socket socket) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String chooseServerKeyIdentityHint(SSLEngine engine) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String chooseClientKeyIdentity(String identityHint, Socket socket) {
|
|
||||||
return identityHint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String chooseClientKeyIdentity(String identityHint, SSLEngine engine) {
|
|
||||||
return identityHint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SecretKey getKey(String identityHint, String identity, Socket socket) {
|
|
||||||
return new SecretKeySpec(presharedKey, "AES");
|
|
||||||
}
|
|
||||||
|
|
||||||
public SecretKey getKey(String identityHint, String identity, SSLEngine engine) {
|
|
||||||
return new SecretKeySpec(presharedKey, "AES");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,19 +21,16 @@ package org.sufficientlysecure.keychain.ui;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
|
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
import org.sufficientlysecure.keychain.util.Preferences;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@@ -48,7 +45,6 @@ public class CreateKeyStartFragment extends Fragment {
|
|||||||
View mImportKey;
|
View mImportKey;
|
||||||
View mSecurityToken;
|
View mSecurityToken;
|
||||||
TextView mSkipOrCancel;
|
TextView mSkipOrCancel;
|
||||||
View mSecureDeviceSetup;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +68,6 @@ public class CreateKeyStartFragment extends Fragment {
|
|||||||
mImportKey = view.findViewById(R.id.create_key_import_button);
|
mImportKey = view.findViewById(R.id.create_key_import_button);
|
||||||
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
|
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
|
||||||
mSkipOrCancel = view.findViewById(R.id.create_key_cancel);
|
mSkipOrCancel = view.findViewById(R.id.create_key_cancel);
|
||||||
mSecureDeviceSetup = view.findViewById(R.id.create_key_secure_device_setup);
|
|
||||||
|
|
||||||
if (mCreateKeyActivity.mFirstTime) {
|
if (mCreateKeyActivity.mFirstTime) {
|
||||||
mSkipOrCancel.setText(R.string.first_time_skip);
|
mSkipOrCancel.setText(R.string.first_time_skip);
|
||||||
@@ -96,15 +91,6 @@ public class CreateKeyStartFragment extends Fragment {
|
|||||||
startActivityForResult(intent, REQUEST_CODE_IMPORT_KEY);
|
startActivityForResult(intent, REQUEST_CODE_IMPORT_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
||||||
mSecureDeviceSetup.setOnClickListener(v -> {
|
|
||||||
TransferFragment frag = new TransferFragment();
|
|
||||||
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
mSecureDeviceSetup.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
mSkipOrCancel.setOnClickListener(v -> {
|
mSkipOrCancel.setOnClickListener(v -> {
|
||||||
if (!mCreateKeyActivity.mFirstTime) {
|
if (!mCreateKeyActivity.mFirstTime) {
|
||||||
mCreateKeyActivity.setResult(Activity.RESULT_CANCELED);
|
mCreateKeyActivity.setResult(Activity.RESULT_CANCELED);
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
|||||||
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
||||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
|
|
||||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||||
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
|
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
|
||||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
|
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
|
||||||
@@ -155,17 +154,6 @@ public class ImportKeysProxyActivity extends FragmentActivity
|
|||||||
|
|
||||||
Timber.d("scanned: " + uri);
|
Timber.d("scanned: " + uri);
|
||||||
|
|
||||||
// example: pgp+transfer:
|
|
||||||
if (uri != null && uri.getScheme() != null && uri.getScheme().equalsIgnoreCase(Constants.SKT_SCHEME)) {
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER);
|
|
||||||
intent.putExtra(TransferFragment.EXTRA_OPENPGP_SKT_INFO, uri);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282
|
// example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282
|
||||||
if (uri == null || uri.getScheme() == null ||
|
if (uri == null || uri.getScheme() == null ||
|
||||||
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {
|
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ package org.sufficientlysecure.keychain.ui;
|
|||||||
|
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -41,8 +38,6 @@ import org.sufficientlysecure.keychain.R;
|
|||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||||
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
|
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
|
||||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.TransferNotAvailableFragment;
|
|
||||||
import org.sufficientlysecure.keychain.util.FabContainer;
|
import org.sufficientlysecure.keychain.util.FabContainer;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
import org.sufficientlysecure.keychain.util.Preferences;
|
||||||
|
|
||||||
@@ -52,7 +47,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
static final int ID_ENCRYPT_DECRYPT = 2;
|
static final int ID_ENCRYPT_DECRYPT = 2;
|
||||||
static final int ID_APPS = 3;
|
static final int ID_APPS = 3;
|
||||||
static final int ID_BACKUP = 4;
|
static final int ID_BACKUP = 4;
|
||||||
public static final int ID_TRANSFER = 5;
|
|
||||||
static final int ID_SETTINGS = 6;
|
static final int ID_SETTINGS = 6;
|
||||||
static final int ID_HELP = 7;
|
static final int ID_HELP = 7;
|
||||||
|
|
||||||
@@ -85,11 +79,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
.withIdentifier(ID_APPS).withSelectable(false),
|
.withIdentifier(ID_APPS).withSelectable(false),
|
||||||
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
|
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
|
||||||
.withIdentifier(ID_BACKUP).withSelectable(false),
|
.withIdentifier(ID_BACKUP).withSelectable(false),
|
||||||
new PrimaryDrawerItem().withName(R.string.nav_transfer)
|
|
||||||
.withIcon(R.drawable.ic_wifi_lock_24dp)
|
|
||||||
.withIconColorRes(R.color.md_grey_600)
|
|
||||||
.withIconTintingEnabled(true)
|
|
||||||
.withIdentifier(ID_TRANSFER).withSelectable(false),
|
|
||||||
new DividerDrawerItem(),
|
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_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)
|
new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false)
|
||||||
@@ -113,9 +102,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
case ID_BACKUP:
|
case ID_BACKUP:
|
||||||
onBackupSelected();
|
onBackupSelected();
|
||||||
break;
|
break;
|
||||||
case ID_TRANSFER:
|
|
||||||
onTransferSelected();
|
|
||||||
break;
|
|
||||||
case ID_SETTINGS:
|
case ID_SETTINGS:
|
||||||
intent = new Intent(MainActivity.this, SettingsActivity.class);
|
intent = new Intent(MainActivity.this, SettingsActivity.class);
|
||||||
break;
|
break;
|
||||||
@@ -168,9 +154,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
case ID_APPS:
|
case ID_APPS:
|
||||||
onAppsSelected();
|
onAppsSelected();
|
||||||
break;
|
break;
|
||||||
case ID_TRANSFER:
|
|
||||||
onTransferSelected();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +173,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
case ID_APPS:
|
case ID_APPS:
|
||||||
onAppsSelected();
|
onAppsSelected();
|
||||||
break;
|
break;
|
||||||
case ID_TRANSFER:
|
|
||||||
onTransferSelected();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,18 +214,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
|
|||||||
setFragment(frag);
|
setFragment(frag);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTransferSelected() {
|
|
||||||
mToolbar.setTitle(R.string.nav_transfer);
|
|
||||||
mDrawer.setSelection(ID_TRANSFER, false);
|
|
||||||
if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
|
|
||||||
Fragment frag = new TransferNotAvailableFragment();
|
|
||||||
setFragment(frag);
|
|
||||||
} else {
|
|
||||||
Fragment frag = new TransferFragment();
|
|
||||||
setFragment(frag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
// add the values which need to be saved from the drawer to the bundle
|
// add the values which need to be saved from the drawer to the bundle
|
||||||
|
|||||||
@@ -299,13 +299,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
|
|||||||
startPassphraseActivity(REQUEST_BACKUP);
|
startPassphraseActivity(REQUEST_BACKUP);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.menu_key_view_skt: {
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_TRANSFER);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.menu_key_view_delete: {
|
case R.id.menu_key_view_delete: {
|
||||||
deleteKey();
|
deleteKey();
|
||||||
return true;
|
return true;
|
||||||
@@ -335,7 +328,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
|
|||||||
}
|
}
|
||||||
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
|
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
|
||||||
backupKey.setVisible(unifiedKeyInfo.has_any_secret());
|
backupKey.setVisible(unifiedKeyInfo.has_any_secret());
|
||||||
menu.findItem(R.id.menu_key_view_skt).setVisible(unifiedKeyInfo.has_any_secret());
|
|
||||||
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
|
MenuItem changePassword = menu.findItem(R.id.menu_key_change_password);
|
||||||
changePassword.setVisible(unifiedKeyInfo.has_any_secret());
|
changePassword.setVisible(unifiedKeyInfo.has_any_secret());
|
||||||
|
|
||||||
|
|||||||
@@ -1,477 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.transfer.presenter;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.net.wifi.WifiInfo;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
|
|
||||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
|
||||||
import org.openintents.openpgp.util.OpenPgpUtils.UserId;
|
|
||||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
|
||||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
|
||||||
import org.sufficientlysecure.keychain.network.KeyTransferInteractor;
|
|
||||||
import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
|
|
||||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
|
||||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
|
||||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
|
||||||
import org.sufficientlysecure.keychain.daos.KeyRepository;
|
|
||||||
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
|
|
||||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback;
|
|
||||||
import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.OnClickImportKeyListener;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyAdapter;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyItem;
|
|
||||||
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;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
|
|
||||||
public class TransferPresenter implements KeyTransferCallback, OnClickTransferKeyListener, OnClickImportKeyListener {
|
|
||||||
private static final String DELIMITER_START = "-----BEGIN PGP PRIVATE KEY BLOCK-----";
|
|
||||||
private static final String DELIMITER_END = "-----END PGP PRIVATE KEY BLOCK-----";
|
|
||||||
private static final String BACKSTACK_TAG_TRANSFER = "transfer";
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final TransferMvpView view;
|
|
||||||
private final KeyRepository keyRepository;
|
|
||||||
|
|
||||||
private final TransferKeyAdapter secretKeyAdapter;
|
|
||||||
private final ReceivedKeyAdapter receivedKeyAdapter;
|
|
||||||
private final LifecycleOwner lifecycleOwner;
|
|
||||||
private final GenericViewModel viewModel;
|
|
||||||
|
|
||||||
|
|
||||||
private KeyTransferInteractor keyTransferClientInteractor;
|
|
||||||
private KeyTransferInteractor keyTransferServerInteractor;
|
|
||||||
|
|
||||||
private boolean wasConnected = false;
|
|
||||||
private boolean sentData = false;
|
|
||||||
private boolean waitingForWifi = false;
|
|
||||||
private Long confirmingMasterKeyId;
|
|
||||||
|
|
||||||
|
|
||||||
public TransferPresenter(Context context, LifecycleOwner lifecycleOwner,
|
|
||||||
GenericViewModel viewModel, TransferMvpView view) {
|
|
||||||
this.context = context;
|
|
||||||
this.view = view;
|
|
||||||
this.lifecycleOwner = lifecycleOwner;
|
|
||||||
this.viewModel = viewModel;
|
|
||||||
this.keyRepository = KeyRepository.create(context);
|
|
||||||
|
|
||||||
secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this);
|
|
||||||
view.setSecretKeyAdapter(secretKeyAdapter);
|
|
||||||
|
|
||||||
receivedKeyAdapter = new ReceivedKeyAdapter(context, LayoutInflater.from(context), this);
|
|
||||||
view.setReceivedKeyAdapter(receivedKeyAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void onUiInitFromIntentUri(final Uri initUri) {
|
|
||||||
connectionStartConnect(initUri.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiStart() {
|
|
||||||
LiveData<List<UnifiedKeyInfo>> liveData =
|
|
||||||
viewModel.getGenericLiveData(context, keyRepository::getAllUnifiedKeyInfoWithSecret);
|
|
||||||
liveData.observe(lifecycleOwner, this::onLoadSecretUnifiedKeyInfo);
|
|
||||||
|
|
||||||
if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) {
|
|
||||||
checkWifiResetAndStartListen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLoadSecretUnifiedKeyInfo(List<UnifiedKeyInfo> data) {
|
|
||||||
secretKeyAdapter.setData(data);
|
|
||||||
view.setShowSecretKeyEmptyView(data.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiStop() {
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
if (wasConnected) {
|
|
||||||
view.showViewDisconnected();
|
|
||||||
view.dismissConfirmationIfExists();
|
|
||||||
secretKeyAdapter.setAllDisabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiClickScan() {
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
view.scanQrCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiClickScanAgain() {
|
|
||||||
onUiClickScan();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiClickDone() {
|
|
||||||
view.finishFragmentOrActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiQrCodeScanned(String qrCodeContent) {
|
|
||||||
connectionStartConnect(qrCodeContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiBackStackPop() {
|
|
||||||
if (wasConnected) {
|
|
||||||
checkWifiResetAndStartListen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUiClickTransferKey(long masterKeyId) {
|
|
||||||
if (sentData) {
|
|
||||||
prepareAndSendKey(masterKeyId);
|
|
||||||
} else {
|
|
||||||
confirmingMasterKeyId = masterKeyId;
|
|
||||||
view.showConfirmSendDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUiClickConfirmSend() {
|
|
||||||
if (confirmingMasterKeyId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
long masterKeyId = confirmingMasterKeyId;
|
|
||||||
confirmingMasterKeyId = null;
|
|
||||||
|
|
||||||
prepareAndSendKey(masterKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUiClickImportKey(final long masterKeyId, String keyData) {
|
|
||||||
receivedKeyAdapter.focusItem(masterKeyId);
|
|
||||||
|
|
||||||
final ImportKeyringParcel importKeyringParcel = ImportKeyringParcel.createImportKeyringParcel(
|
|
||||||
ParcelableKeyRing.createFromEncodedBytes(keyData.getBytes()));
|
|
||||||
|
|
||||||
CryptoOperationHelper<ImportKeyringParcel,ImportKeyResult> op =
|
|
||||||
view.createCryptoOperationHelper(new Callback<ImportKeyringParcel,ImportKeyResult>() {
|
|
||||||
@Override
|
|
||||||
public ImportKeyringParcel createOperationInput() {
|
|
||||||
return importKeyringParcel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCryptoOperationSuccess(ImportKeyResult result) {
|
|
||||||
receivedKeyAdapter.focusItem(null);
|
|
||||||
receivedKeyAdapter.addToFinishedItems(masterKeyId);
|
|
||||||
view.releaseCryptoOperationHelper();
|
|
||||||
view.showResultNotification(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCryptoOperationCancelled() {
|
|
||||||
view.releaseCryptoOperationHelper();
|
|
||||||
receivedKeyAdapter.focusItem(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCryptoOperationError(ImportKeyResult result) {
|
|
||||||
receivedKeyAdapter.focusItem(null);
|
|
||||||
view.releaseCryptoOperationHelper();
|
|
||||||
view.showResultNotification(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCryptoSetProgress(String msg, int progress, int max) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
op.cryptoOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onWifiConnected() {
|
|
||||||
if (waitingForWifi) {
|
|
||||||
resetAndStartListen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServerStarted(String qrCodeData) {
|
|
||||||
Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse(qrCodeData));
|
|
||||||
view.setQrImage(qrCodeBitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionEstablished(String otherName) {
|
|
||||||
wasConnected = true;
|
|
||||||
|
|
||||||
secretKeyAdapter.clearFinishedItems();
|
|
||||||
secretKeyAdapter.focusItem(null);
|
|
||||||
secretKeyAdapter.setAllDisabled(false);
|
|
||||||
receivedKeyAdapter.clear();
|
|
||||||
|
|
||||||
view.showConnectionEstablished(otherName);
|
|
||||||
view.setShowDoneIcon(true);
|
|
||||||
view.addFakeBackStackItem(BACKSTACK_TAG_TRANSFER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionLost() {
|
|
||||||
if (!wasConnected) {
|
|
||||||
checkWifiResetAndStartListen();
|
|
||||||
|
|
||||||
view.showErrorConnectionFailed();
|
|
||||||
} else {
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
view.dismissConfirmationIfExists();
|
|
||||||
view.showViewDisconnected();
|
|
||||||
secretKeyAdapter.setAllDisabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDataReceivedOk(String receivedData) {
|
|
||||||
if (sentData) {
|
|
||||||
Timber.d("received data, but we already sent a key! race condition, or other side misbehaving?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("received data");
|
|
||||||
UncachedKeyRing uncachedKeyRing;
|
|
||||||
try {
|
|
||||||
uncachedKeyRing = UncachedKeyRing.decodeFromData(receivedData.getBytes());
|
|
||||||
} catch (PgpGeneralException | IOException | RuntimeException e) {
|
|
||||||
Timber.e(e, "error parsing incoming key");
|
|
||||||
view.showErrorBadKey();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String primaryUserId = uncachedKeyRing.getPublicKey().getPrimaryUserIdWithFallback();
|
|
||||||
UserId userId = OpenPgpUtils.splitUserId(primaryUserId);
|
|
||||||
|
|
||||||
ReceivedKeyItem receivedKeyItem = new ReceivedKeyItem(receivedData, uncachedKeyRing.getMasterKeyId(),
|
|
||||||
uncachedKeyRing.getCreationTime(), userId.name, userId.email);
|
|
||||||
receivedKeyAdapter.addItem(receivedKeyItem);
|
|
||||||
|
|
||||||
view.showReceivingKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDataSentOk(String passthrough) {
|
|
||||||
Timber.d("data sent ok!");
|
|
||||||
final long masterKeyId = Long.parseLong(passthrough);
|
|
||||||
|
|
||||||
new Handler().postDelayed(() -> {
|
|
||||||
secretKeyAdapter.focusItem(null);
|
|
||||||
secretKeyAdapter.addToFinishedItems(masterKeyId);
|
|
||||||
}, 750);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorConnect() {
|
|
||||||
view.showWaitingForConnection();
|
|
||||||
view.showErrorConnectionFailed();
|
|
||||||
|
|
||||||
resetAndStartListen();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
String ownWifiSsid = getConnectedWifiSsid();
|
|
||||||
if (!wifiSsid.equalsIgnoreCase(ownWifiSsid)) {
|
|
||||||
view.showWifiError(wifiSsid);
|
|
||||||
} else {
|
|
||||||
view.showWaitingForConnection();
|
|
||||||
view.showErrorConnectionFailed();
|
|
||||||
|
|
||||||
resetAndStartListen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorListen() {
|
|
||||||
view.showErrorListenFailed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionError(String errorMessage) {
|
|
||||||
view.showErrorConnectionError(errorMessage);
|
|
||||||
|
|
||||||
connectionClear();
|
|
||||||
if (wasConnected) {
|
|
||||||
view.showViewDisconnected();
|
|
||||||
secretKeyAdapter.setAllDisabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connectionStartConnect(String qrCodeContent) {
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
view.showEstablishingConnection();
|
|
||||||
|
|
||||||
keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
|
|
||||||
try {
|
|
||||||
keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this);
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
view.showErrorConnectionFailed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkWifiResetAndStartListen() {
|
|
||||||
if (!isWifiConnected()) {
|
|
||||||
waitingForWifi = true;
|
|
||||||
view.showNotOnWifi();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAndStartListen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetAndStartListen() {
|
|
||||||
waitingForWifi = false;
|
|
||||||
wasConnected = false;
|
|
||||||
sentData = false;
|
|
||||||
connectionClear();
|
|
||||||
|
|
||||||
String wifiSsid = getConnectedWifiSsid();
|
|
||||||
|
|
||||||
keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
|
|
||||||
keyTransferServerInteractor.startServer(this, wifiSsid);
|
|
||||||
|
|
||||||
view.showWaitingForConnection();
|
|
||||||
view.setShowDoneIcon(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWifiConnected() {
|
|
||||||
ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
if (connManager == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkInfo wifiNetwork = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
|
||||||
|
|
||||||
return wifiNetwork.isConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getConnectedWifiSsid() {
|
|
||||||
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
|
||||||
if (wifiManager == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
WifiInfo info = wifiManager.getConnectionInfo();
|
|
||||||
if (info == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// getSSID will return the ssid in quotes if it is valid utf-8. we only return it in that case.
|
|
||||||
String ssid = info.getSSID();
|
|
||||||
if (ssid.charAt(0) != '"') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ssid.substring(1, ssid.length() -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connectionClear() {
|
|
||||||
if (keyTransferServerInteractor != null) {
|
|
||||||
keyTransferServerInteractor.closeConnection();
|
|
||||||
keyTransferServerInteractor = null;
|
|
||||||
}
|
|
||||||
if (keyTransferClientInteractor != null) {
|
|
||||||
keyTransferClientInteractor.closeConnection();
|
|
||||||
keyTransferClientInteractor = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prepareAndSendKey(long masterKeyId) {
|
|
||||||
try {
|
|
||||||
byte[] armoredSecretKey = keyRepository.getSecretKeyRingAsArmoredData(masterKeyId);
|
|
||||||
secretKeyAdapter.focusItem(masterKeyId);
|
|
||||||
connectionSend(armoredSecretKey, Long.toString(masterKeyId));
|
|
||||||
} catch (IOException | NotFoundException e) {
|
|
||||||
// TODO
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connectionSend(byte[] armoredSecretKey, String passthrough) {
|
|
||||||
sentData = true;
|
|
||||||
if (keyTransferClientInteractor != null) {
|
|
||||||
keyTransferClientInteractor.sendData(armoredSecretKey, passthrough);
|
|
||||||
} else if (keyTransferServerInteractor != null) {
|
|
||||||
keyTransferServerInteractor.sendData(armoredSecretKey, passthrough);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface TransferMvpView {
|
|
||||||
void showNotOnWifi();
|
|
||||||
void showWaitingForConnection();
|
|
||||||
void showEstablishingConnection();
|
|
||||||
void showConnectionEstablished(String hostname);
|
|
||||||
|
|
||||||
void showWifiError(String wifiSsid);
|
|
||||||
|
|
||||||
void showReceivingKeys();
|
|
||||||
|
|
||||||
void showViewDisconnected();
|
|
||||||
|
|
||||||
void scanQrCode();
|
|
||||||
void setQrImage(Bitmap qrCode);
|
|
||||||
|
|
||||||
void releaseCryptoOperationHelper();
|
|
||||||
|
|
||||||
void showErrorBadKey();
|
|
||||||
void showErrorConnectionFailed();
|
|
||||||
void showErrorListenFailed();
|
|
||||||
void showErrorConnectionError(String errorMessage);
|
|
||||||
void showResultNotification(ImportKeyResult result);
|
|
||||||
|
|
||||||
void setShowDoneIcon(boolean showDoneIcon);
|
|
||||||
|
|
||||||
void setSecretKeyAdapter(Adapter adapter);
|
|
||||||
void setShowSecretKeyEmptyView(boolean isEmpty);
|
|
||||||
void setReceivedKeyAdapter(Adapter secretKeyAdapter);
|
|
||||||
|
|
||||||
<T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback);
|
|
||||||
|
|
||||||
void addFakeBackStackItem(String tag);
|
|
||||||
|
|
||||||
void finishFragmentOrActivity();
|
|
||||||
|
|
||||||
void showConfirmSendDialog();
|
|
||||||
void dismissConfirmationIfExists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.transfer.view;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.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 android.widget.ViewAnimator;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
|
||||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
|
|
||||||
|
|
||||||
|
|
||||||
public class ReceivedSecretKeyList extends RecyclerView {
|
|
||||||
private static final int STATE_INVISIBLE = 0;
|
|
||||||
// private static final int STATE_BUTTON = 1; // used in TransferSecretKeyList
|
|
||||||
private static final int STATE_PROGRESS = 2;
|
|
||||||
private static final int STATE_TRANSFERRED = 3;
|
|
||||||
private static final int STATE_IMPORT_BUTTON = 4;
|
|
||||||
|
|
||||||
|
|
||||||
public ReceivedSecretKeyList(Context context) {
|
|
||||||
super(context);
|
|
||||||
init(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReceivedSecretKeyList(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReceivedSecretKeyList(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, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ReceivedKeyAdapter extends Adapter<ReceivedKeyViewHolder> {
|
|
||||||
private final Context context;
|
|
||||||
private final LayoutInflater layoutInflater;
|
|
||||||
private final OnClickImportKeyListener onClickImportKeyListener;
|
|
||||||
|
|
||||||
private Long focusedMasterKeyId;
|
|
||||||
private List<ReceivedKeyItem> data = new ArrayList<>();
|
|
||||||
private ArrayList<Long> finishedItems = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
public ReceivedKeyAdapter(Context context, LayoutInflater layoutInflater,
|
|
||||||
OnClickImportKeyListener onClickImportKeyListener) {
|
|
||||||
this.context = context;
|
|
||||||
this.layoutInflater = layoutInflater;
|
|
||||||
this.onClickImportKeyListener = onClickImportKeyListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReceivedKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
return new ReceivedKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(ReceivedKeyViewHolder holder, int position) {
|
|
||||||
ReceivedKeyItem item = data.get(position);
|
|
||||||
boolean isFinished = finishedItems.contains(item.masterKeyId);
|
|
||||||
holder.bind(context, item, onClickImportKeyListener, focusedMasterKeyId, isFinished);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return data != null ? data.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return data.get(position).masterKeyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addToFinishedItems(long masterKeyId) {
|
|
||||||
finishedItems.add(masterKeyId);
|
|
||||||
// doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things!
|
|
||||||
}
|
|
||||||
|
|
||||||
public void focusItem(Long masterKeyId) {
|
|
||||||
focusedMasterKeyId = masterKeyId;
|
|
||||||
notifyItemRangeChanged(0, getItemCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItem(ReceivedKeyItem receivedKeyItem) {
|
|
||||||
data.add(receivedKeyItem);
|
|
||||||
notifyItemInserted(data.size() -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
data.clear();
|
|
||||||
finishedItems.clear();
|
|
||||||
focusedMasterKeyId = null;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ReceivedKeyViewHolder extends ViewHolder {
|
|
||||||
private final TextView vName;
|
|
||||||
private final TextView vEmail;
|
|
||||||
private final TextView vCreation;
|
|
||||||
private final View vImportButton;
|
|
||||||
private final ViewAnimator vState;
|
|
||||||
|
|
||||||
ReceivedKeyViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
|
|
||||||
vName = itemView.findViewById(R.id.key_list_item_name);
|
|
||||||
vEmail = itemView.findViewById(R.id.key_list_item_email);
|
|
||||||
vCreation = itemView.findViewById(R.id.key_list_item_creation);
|
|
||||||
|
|
||||||
vImportButton = itemView.findViewById(R.id.button_import);
|
|
||||||
vState = itemView.findViewById(R.id.transfer_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bind(Context context, final ReceivedKeyItem item,
|
|
||||||
final OnClickImportKeyListener onClickReceiveKeyListener, Long focusedMasterKeyId,
|
|
||||||
boolean isFinished) {
|
|
||||||
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 (focusedMasterKeyId != null) {
|
|
||||||
if (focusedMasterKeyId != item.masterKeyId) {
|
|
||||||
itemView.animate().alpha(0.2f).start();
|
|
||||||
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE);
|
|
||||||
} else {
|
|
||||||
itemView.setAlpha(1.0f);
|
|
||||||
vState.setDisplayedChild(STATE_PROGRESS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemView.animate().alpha(1.0f).start();
|
|
||||||
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_IMPORT_BUTTON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusedMasterKeyId == null && onClickReceiveKeyListener != null) {
|
|
||||||
vImportButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
onClickReceiveKeyListener.onUiClickImportKey(item.masterKeyId, item.keyData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
vImportButton.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnClickImportKeyListener {
|
|
||||||
void onUiClickImportKey(long masterKeyId, String keyData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ReceivedKeyItem {
|
|
||||||
private final String keyData;
|
|
||||||
|
|
||||||
private final long masterKeyId;
|
|
||||||
private final long creationMillis;
|
|
||||||
private final String name;
|
|
||||||
private final String email;
|
|
||||||
|
|
||||||
public ReceivedKeyItem(String keyData, long masterKeyId, long creationMillis, String name, String email) {
|
|
||||||
this.keyData = keyData;
|
|
||||||
this.masterKeyId = masterKeyId;
|
|
||||||
this.creationMillis = creationMillis;
|
|
||||||
this.name = name;
|
|
||||||
this.email = email;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.transfer.view;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnDismissListener;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AlertDialog.Builder;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
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 com.google.zxing.client.android.Intents;
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
|
||||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
|
||||||
import org.sufficientlysecure.keychain.ui.MainActivity;
|
|
||||||
import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity;
|
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
|
||||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback;
|
|
||||||
import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter;
|
|
||||||
import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView;
|
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
|
||||||
import org.sufficientlysecure.keychain.ui.widget.ConnectionStatusView;
|
|
||||||
import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
|
|
||||||
public class TransferFragment extends Fragment implements TransferMvpView {
|
|
||||||
public static final int REQUEST_CODE_SCAN = 1;
|
|
||||||
public static final int LOADER_ID = 1;
|
|
||||||
|
|
||||||
public static final String EXTRA_OPENPGP_SKT_INFO = "openpgp_skt_info";
|
|
||||||
|
|
||||||
|
|
||||||
private ImageView vQrCodeImage;
|
|
||||||
private TransferPresenter presenter;
|
|
||||||
private ToolableViewAnimator vTransferAnimator;
|
|
||||||
private TextView vConnectionStatusText1;
|
|
||||||
private TextView vConnectionStatusText2;
|
|
||||||
private ConnectionStatusView vConnectionStatusView1;
|
|
||||||
private ConnectionStatusView vConnectionStatusView2;
|
|
||||||
private RecyclerView vTransferKeyList;
|
|
||||||
private View vTransferKeyListEmptyView;
|
|
||||||
private RecyclerView vReceivedKeyList;
|
|
||||||
|
|
||||||
private CryptoOperationHelper currentCryptoOperationHelper;
|
|
||||||
private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
|
|
||||||
NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
|
|
||||||
if (networkInfo != null && networkInfo.isConnected()) {
|
|
||||||
presenter.onWifiConnected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private boolean showDoneIcon;
|
|
||||||
private AlertDialog confirmationDialog;
|
|
||||||
private TextView vWifiErrorInstructions;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.transfer_fragment, container, false);
|
|
||||||
|
|
||||||
vTransferAnimator = view.findViewById(R.id.transfer_animator);
|
|
||||||
|
|
||||||
vConnectionStatusText1 = view.findViewById(R.id.connection_status_1);
|
|
||||||
vConnectionStatusText2 = view.findViewById(R.id.connection_status_2);
|
|
||||||
vConnectionStatusView1 = view.findViewById(R.id.connection_status_icon_1);
|
|
||||||
vConnectionStatusView2 = view.findViewById(R.id.connection_status_icon_2);
|
|
||||||
vTransferKeyList = view.findViewById(R.id.transfer_key_list);
|
|
||||||
vTransferKeyListEmptyView = view.findViewById(R.id.transfer_key_list_empty);
|
|
||||||
vReceivedKeyList = view.findViewById(R.id.received_key_list);
|
|
||||||
vWifiErrorInstructions = view.findViewById(R.id.transfer_wifi_error_instructions);
|
|
||||||
|
|
||||||
vQrCodeImage = view.findViewById(R.id.qr_code_image);
|
|
||||||
|
|
||||||
view.findViewById(R.id.button_scan).setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (presenter != null) {
|
|
||||||
presenter.onUiClickScan();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view.findViewById(R.id.button_scan_again).setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (presenter != null) {
|
|
||||||
presenter.onUiClickScanAgain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
GenericViewModel genericViewModel = ViewModelProviders.of(this).get(GenericViewModel.class);
|
|
||||||
presenter = new TransferPresenter(getContext(), this, genericViewModel, this);
|
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onActivityCreated(savedInstanceState);
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent activityIntent = getActivity().getIntent();
|
|
||||||
if (activityIntent != null && activityIntent.hasExtra(EXTRA_OPENPGP_SKT_INFO)) {
|
|
||||||
presenter.onUiInitFromIntentUri(activityIntent.getParcelableExtra(EXTRA_OPENPGP_SKT_INFO));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
|
|
||||||
presenter.onUiStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
|
||||||
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
|
|
||||||
getContext().registerReceiver(broadcastReceiver, intentFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
getContext().unregisterReceiver(broadcastReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
presenter.onUiStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
if (showDoneIcon) {
|
|
||||||
inflater.inflate(R.menu.transfer_menu, menu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.menu_done) {
|
|
||||||
presenter.onUiClickDone();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showNotOnWifi() {
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_no_wifi);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showWaitingForConnection() {
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_waiting);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showEstablishingConnection() {
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connecting);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showConnectionEstablished(String hostname) {
|
|
||||||
// String statusText = getString(R.string.transfer_status_connected, hostname);
|
|
||||||
|
|
||||||
vConnectionStatusText1.setText(R.string.transfer_status_connected);
|
|
||||||
vConnectionStatusText2.setText(R.string.transfer_status_connected);
|
|
||||||
|
|
||||||
vConnectionStatusView1.setConnected(true);
|
|
||||||
vConnectionStatusView2.setConnected(true);
|
|
||||||
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_connected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showWifiError(String wifiSsid) {
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_wifi_error);
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(wifiSsid)) {
|
|
||||||
vWifiErrorInstructions
|
|
||||||
.setText(getResources().getString(R.string.transfer_error_wifi_text_instructions_ssid, wifiSsid));
|
|
||||||
} else {
|
|
||||||
vWifiErrorInstructions.setText(R.string.transfer_error_wifi_text_instructions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showReceivingKeys() {
|
|
||||||
vTransferAnimator.setDisplayedChildId(R.id.transfer_layout_passive);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showViewDisconnected() {
|
|
||||||
vConnectionStatusText1.setText(R.string.transfer_status_disconnected);
|
|
||||||
vConnectionStatusText2.setText(R.string.transfer_status_disconnected);
|
|
||||||
|
|
||||||
vConnectionStatusView1.setConnected(false);
|
|
||||||
vConnectionStatusView2.setConnected(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setQrImage(final Bitmap qrCode) {
|
|
||||||
vQrCodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
|
|
||||||
new OnGlobalLayoutListener() {
|
|
||||||
@Override
|
|
||||||
public void onGlobalLayout() {
|
|
||||||
int viewSize = vQrCodeImage.getWidth();
|
|
||||||
if (viewSize == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// create actual bitmap in display dimensions
|
|
||||||
Bitmap scaled = Bitmap.createScaledBitmap(qrCode, viewSize, viewSize, false);
|
|
||||||
vQrCodeImage.setImageBitmap(scaled);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
vQrCodeImage.requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scanQrCode() {
|
|
||||||
Intent intent = new Intent(getActivity(), QrCodeCaptureActivity.class);
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_SCAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setShowDoneIcon(boolean showDoneIcon) {
|
|
||||||
this.showDoneIcon = showDoneIcon;
|
|
||||||
FragmentActivity activity = getActivity();
|
|
||||||
if (activity != null) {
|
|
||||||
activity.invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSecretKeyAdapter(Adapter adapter) {
|
|
||||||
vTransferKeyList.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setShowSecretKeyEmptyView(boolean isEmpty) {
|
|
||||||
vTransferKeyListEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setReceivedKeyAdapter(Adapter adapter) {
|
|
||||||
vReceivedKeyList.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T extends Parcelable, S extends OperationResult> CryptoOperationHelper<T,S> createCryptoOperationHelper(Callback<T, S> callback) {
|
|
||||||
CryptoOperationHelper<T,S> cryptoOperationHelper = new CryptoOperationHelper<>(1, this, callback, null);
|
|
||||||
currentCryptoOperationHelper = cryptoOperationHelper;
|
|
||||||
return cryptoOperationHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void releaseCryptoOperationHelper() {
|
|
||||||
currentCryptoOperationHelper = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showErrorBadKey() {
|
|
||||||
Notify.create(getActivity(), R.string.transfer_error_read_incoming, Style.ERROR).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showErrorConnectionFailed() {
|
|
||||||
Notify.create(getActivity(), R.string.transfer_error_connect, Style.ERROR).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showErrorListenFailed() {
|
|
||||||
Notify.create(getActivity(), R.string.transfer_error_listen, Style.ERROR).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showErrorConnectionError(String errorMessage) {
|
|
||||||
if (errorMessage != null) {
|
|
||||||
String text = getString(R.string.transfer_error_generic_msg, errorMessage);
|
|
||||||
Notify.create(getActivity(), text, Style.ERROR).show();
|
|
||||||
} else {
|
|
||||||
Notify.create(getActivity(), R.string.transfer_error_generic, Style.ERROR).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showResultNotification(ImportKeyResult result) {
|
|
||||||
result.createNotify(getActivity()).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addFakeBackStackItem(final String tag) {
|
|
||||||
FragmentManager fragmentManager = getFragmentManager();
|
|
||||||
|
|
||||||
fragmentManager.beginTransaction()
|
|
||||||
.addToBackStack(tag)
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
fragmentManager.executePendingTransactions();
|
|
||||||
|
|
||||||
fragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
FragmentManager fragMan = getFragmentManager();
|
|
||||||
if (fragMan == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fragMan.popBackStack(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
|
||||||
fragMan.removeOnBackStackChangedListener(this);
|
|
||||||
|
|
||||||
presenter.onUiBackStackPop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void finishFragmentOrActivity() {
|
|
||||||
FragmentActivity activity = getActivity();
|
|
||||||
if (activity != null) {
|
|
||||||
if (activity instanceof MainActivity) {
|
|
||||||
((MainActivity) activity).onKeysSelected();
|
|
||||||
} else {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showConfirmSendDialog() {
|
|
||||||
if (confirmationDialog != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
confirmationDialog = new Builder(getContext())
|
|
||||||
.setTitle(R.string.transfer_confirm_title)
|
|
||||||
.setMessage(R.string.transfer_confirm_text)
|
|
||||||
.setPositiveButton(R.string.transfer_confirm_ok, new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton) {
|
|
||||||
if (whichButton == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
presenter.onUiClickConfirmSend();
|
|
||||||
} else {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.transfer_confirm_cancel, null)
|
|
||||||
.setOnDismissListener(new OnDismissListener() {
|
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
confirmationDialog = null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.create();
|
|
||||||
confirmationDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dismissConfirmationIfExists() {
|
|
||||||
if (confirmationDialog != null && confirmationDialog.isShowing()) {
|
|
||||||
confirmationDialog.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (currentCryptoOperationHelper != null &&
|
|
||||||
currentCryptoOperationHelper.handleActivityResult(requestCode, resultCode, data)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (requestCode) {
|
|
||||||
case REQUEST_CODE_SCAN:
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
String qrCodeData = data.getStringExtra(Intents.Scan.RESULT);
|
|
||||||
presenter.onUiQrCodeScanned(qrCodeData);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.transfer.view;
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
|
||||||
|
|
||||||
|
|
||||||
public class TransferNotAvailableFragment extends Fragment {
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.transfer_not_available_fragment, container, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
||||||
*
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.transfer.view;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.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 android.widget.ViewAnimator;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
|
||||||
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
|
|
||||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
|
|
||||||
|
|
||||||
|
|
||||||
public class TransferSecretKeyList extends RecyclerView {
|
|
||||||
private static final int STATE_INVISIBLE = 0;
|
|
||||||
private static final int STATE_BUTTON = 1;
|
|
||||||
private static final int STATE_PROGRESS = 2;
|
|
||||||
private static final int STATE_TRANSFERRED = 3;
|
|
||||||
// private static final int STATE_IMPORT_BUTTON = 4; // used in ReceivedSecretKeyList
|
|
||||||
|
|
||||||
|
|
||||||
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, true));
|
|
||||||
setItemAnimator(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TransferKeyAdapter extends RecyclerView.Adapter<TransferKeyViewHolder> {
|
|
||||||
private final Context context;
|
|
||||||
private final LayoutInflater layoutInflater;
|
|
||||||
private final OnClickTransferKeyListener onClickTransferKeyListener;
|
|
||||||
|
|
||||||
private Long focusedMasterKeyId;
|
|
||||||
private List<UnifiedKeyInfo> data;
|
|
||||||
private ArrayList<Long> finishedItems = new ArrayList<>();
|
|
||||||
private boolean allItemsDisabled;
|
|
||||||
|
|
||||||
|
|
||||||
public TransferKeyAdapter(Context context, LayoutInflater layoutInflater,
|
|
||||||
OnClickTransferKeyListener onClickTransferKeyListener) {
|
|
||||||
this.context = context;
|
|
||||||
this.layoutInflater = layoutInflater;
|
|
||||||
this.onClickTransferKeyListener = onClickTransferKeyListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public TransferKeyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull TransferKeyViewHolder holder, int position) {
|
|
||||||
UnifiedKeyInfo item = data.get(position);
|
|
||||||
boolean isFinished = finishedItems.contains(item.master_key_id());
|
|
||||||
holder.bind(context, item, onClickTransferKeyListener, focusedMasterKeyId, isFinished, allItemsDisabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return data != null ? data.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return data.get(position).master_key_id();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(List<UnifiedKeyInfo> data) {
|
|
||||||
this.data = data;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearFinishedItems() {
|
|
||||||
finishedItems.clear();
|
|
||||||
notifyItemRangeChanged(0, getItemCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addToFinishedItems(long masterKeyId) {
|
|
||||||
finishedItems.add(masterKeyId);
|
|
||||||
// doeesn't notify, because it's non-trivial and this is called in conjunction with other refreshing things!
|
|
||||||
}
|
|
||||||
|
|
||||||
public void focusItem(Long masterKeyId) {
|
|
||||||
focusedMasterKeyId = masterKeyId;
|
|
||||||
notifyItemRangeChanged(0, getItemCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllDisabled(boolean allItemsdisablde) {
|
|
||||||
allItemsDisabled = allItemsdisablde;
|
|
||||||
notifyItemRangeChanged(0, getItemCount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class TransferKeyViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private final TextView vName;
|
|
||||||
private final TextView vEmail;
|
|
||||||
private final TextView vCreation;
|
|
||||||
private final View vSendButton;
|
|
||||||
private final ViewAnimator vState;
|
|
||||||
|
|
||||||
TransferKeyViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
|
|
||||||
vName = itemView.findViewById(R.id.key_list_item_name);
|
|
||||||
vEmail = itemView.findViewById(R.id.key_list_item_email);
|
|
||||||
vCreation = itemView.findViewById(R.id.key_list_item_creation);
|
|
||||||
|
|
||||||
vSendButton = itemView.findViewById(R.id.button_transfer);
|
|
||||||
vState = itemView.findViewById(R.id.transfer_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bind(Context context, UnifiedKeyInfo item,
|
|
||||||
final OnClickTransferKeyListener onClickTransferKeyListener, Long focusedMasterKeyId,
|
|
||||||
boolean isFinished, boolean disableAll) {
|
|
||||||
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.creation() * 1000,
|
|
||||||
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 (disableAll) {
|
|
||||||
itemView.setAlpha(0.2f);
|
|
||||||
vState.setDisplayedChild(STATE_INVISIBLE);
|
|
||||||
vSendButton.setOnClickListener(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusedMasterKeyId != null) {
|
|
||||||
if (focusedMasterKeyId != item.master_key_id()) {
|
|
||||||
itemView.animate().alpha(0.2f).start();
|
|
||||||
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE);
|
|
||||||
} else {
|
|
||||||
itemView.setAlpha(1.0f);
|
|
||||||
vState.setDisplayedChild(STATE_PROGRESS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemView.animate().alpha(1.0f).start();
|
|
||||||
vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_BUTTON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusedMasterKeyId == null && onClickTransferKeyListener != null) {
|
|
||||||
vSendButton.setOnClickListener(
|
|
||||||
v -> onClickTransferKeyListener.onUiClickTransferKey(item.master_key_id()));
|
|
||||||
} else {
|
|
||||||
vSendButton.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnClickTransferKeyListener {
|
|
||||||
void onUiClickTransferKey(long masterKeyId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -116,24 +116,6 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
style="?android:attr/borderlessButtonStyle" />
|
style="?android:attr/borderlessButtonStyle" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/create_key_secure_device_setup"
|
|
||||||
android:paddingLeft="16dp"
|
|
||||||
android:paddingRight="16dp"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/first_time_secure_wifi_transfer"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
|
||||||
android:drawableRight="@drawable/ic_chevron_right_grey_24dp"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:gravity="right|center_vertical"
|
|
||||||
android:clickable="true"
|
|
||||||
style="?android:attr/borderlessButtonStyle" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/create_key_cancel"
|
android:id="@+id/create_key_cancel"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
|
|||||||
@@ -18,11 +18,6 @@
|
|||||||
android:title="@string/menu_export_key"
|
android:title="@string/menu_export_key"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_key_view_skt"
|
|
||||||
android:title="@string/menu_secure_wifi_transfer"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_key_view_delete"
|
android:id="@+id/menu_key_view_delete"
|
||||||
android:icon="@drawable/ic_delete_grey_24dp"
|
android:icon="@drawable/ic_delete_grey_24dp"
|
||||||
|
|||||||
@@ -811,7 +811,6 @@
|
|||||||
<string name="drawer_close">"Close navigation drawer"</string>
|
<string name="drawer_close">"Close navigation drawer"</string>
|
||||||
<string name="my_keys">"My Keys"</string>
|
<string name="my_keys">"My Keys"</string>
|
||||||
<string name="nav_backup">"Backup/Restore"</string>
|
<string name="nav_backup">"Backup/Restore"</string>
|
||||||
<string name="nav_transfer">"Secure Wifi Transfer"</string>
|
|
||||||
|
|
||||||
<!-- hints -->
|
<!-- hints -->
|
||||||
<string name="encrypt_content_edit_text_hint">"Type text"</string>
|
<string name="encrypt_content_edit_text_hint">"Type text"</string>
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.network;
|
|
||||||
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import junit.framework.Assert;
|
|
||||||
import org.robolectric.shadows.ShadowLog;
|
|
||||||
import org.robolectric.shadows.ShadowLooper;
|
|
||||||
import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
|
|
||||||
|
|
||||||
import static junit.framework.Assert.assertTrue;
|
|
||||||
import static junit.framework.Assert.fail;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
// disabled, because we can't easily mock the tls-psk ciphersuite (it's removed in bouncycastle) :(
|
|
||||||
//@RunWith(KeychainTestRunner.class)
|
|
||||||
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
|
|
||||||
public class KeyTransferInteractorTest {
|
|
||||||
private static final String DELIM_START = "--";
|
|
||||||
private static final String DELIM_END = "--";
|
|
||||||
|
|
||||||
private String receivedQrCodeData;
|
|
||||||
private boolean clientConnectionEstablished;
|
|
||||||
private boolean serverConnectionEstablished;
|
|
||||||
|
|
||||||
// @Before
|
|
||||||
public void setUp() throws Exception {
|
|
||||||
ShadowLog.stream = System.out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Test
|
|
||||||
public void testServerShouldGiveSuccessCallback() throws URISyntaxException {
|
|
||||||
KeyTransferInteractor serverKeyTransferInteractor = new KeyTransferInteractor(DELIM_START, DELIM_END);
|
|
||||||
|
|
||||||
serverKeyTransferInteractor.startServer(new SimpleKeyTransferCallback() {
|
|
||||||
@Override
|
|
||||||
public void onServerStarted(String qrCodeData) {
|
|
||||||
receivedQrCodeData = qrCodeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionEstablished(String otherName) {
|
|
||||||
serverConnectionEstablished = true;
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
waitForLooperCallback();
|
|
||||||
Assert.assertNotNull(receivedQrCodeData);
|
|
||||||
|
|
||||||
final KeyTransferInteractor clientKeyTransferInteractor = new KeyTransferInteractor(DELIM_START, DELIM_END);
|
|
||||||
clientKeyTransferInteractor.connectToServer(receivedQrCodeData, new SimpleKeyTransferCallback() {
|
|
||||||
@Override
|
|
||||||
public void onConnectionEstablished(String otherName) {
|
|
||||||
clientConnectionEstablished = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
waitForLooperCallback();
|
|
||||||
waitForLooperCallback();
|
|
||||||
|
|
||||||
assertTrue(clientConnectionEstablished);
|
|
||||||
assertTrue(serverConnectionEstablished);
|
|
||||||
|
|
||||||
serverKeyTransferInteractor.sendData(new byte[] { (byte) 1, (byte) 2 }, "passthrough");
|
|
||||||
waitForLooperCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitForLooperCallback() {
|
|
||||||
while (!ShadowLooper.getShadowMainLooper().getScheduler().runOneTask());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static class SimpleKeyTransferCallback implements KeyTransferCallback {
|
|
||||||
@Override
|
|
||||||
public void onServerStarted(String qrCodeData) {
|
|
||||||
fail("unexpected callback: onServerStarted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionEstablished(String otherName) {
|
|
||||||
fail("unexpected callback: onConnectionEstablished");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionLost() {
|
|
||||||
fail("unexpected callback: onConnectionLost");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDataReceivedOk(String receivedData) {
|
|
||||||
fail("unexpected callback: onDataReceivedOk");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDataSentOk(String passthrough) {
|
|
||||||
fail("unexpected callback: onDataSentOk");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
|
|
||||||
fail("unexpected callback: onConnectionErrorNoRouteToHost");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorConnect() {
|
|
||||||
fail("unexpected callback: onConnectionErrorConnect");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionErrorListen() {
|
|
||||||
fail("unexpected callback: onConnectionErrorListen");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionError(String arg) {
|
|
||||||
fail("unexpected callback: onConnectionError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.network;
|
|
||||||
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
|
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
public class SktUriTest {
|
|
||||||
static final String HOST = "127.0.0.1";
|
|
||||||
static final int PORT = 1234;
|
|
||||||
static final byte[] PRESHARED_KEY = { 1, 2 };
|
|
||||||
static final String SSID = "ssid";
|
|
||||||
|
|
||||||
static final String ENCODED_SKT = String.format("OPGPSKT:%s/%d/%s/SSID:%s",
|
|
||||||
HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes()));
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreate() {
|
|
||||||
SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, null);
|
|
||||||
|
|
||||||
assertEquals(HOST, sktUri.getHost());
|
|
||||||
assertEquals(PORT, sktUri.getPort());
|
|
||||||
assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
|
|
||||||
assertEquals(null, sktUri.getWifiSsid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateWithSsid() {
|
|
||||||
SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
|
|
||||||
|
|
||||||
assertEquals(HOST, sktUri.getHost());
|
|
||||||
assertEquals(PORT, sktUri.getPort());
|
|
||||||
assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
|
|
||||||
assertEquals(SSID, sktUri.getWifiSsid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreate_isAllUppercase() {
|
|
||||||
SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
|
|
||||||
|
|
||||||
String encodedSktUri = sktUri.toUriString();
|
|
||||||
assertEquals(encodedSktUri.toUpperCase(), encodedSktUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParse() throws URISyntaxException {
|
|
||||||
SktUri sktUri = SktUri.parse(ENCODED_SKT);
|
|
||||||
|
|
||||||
assertNotNull(sktUri);
|
|
||||||
assertEquals(HOST, sktUri.getHost());
|
|
||||||
assertEquals(PORT, sktUri.getPort());
|
|
||||||
assertArrayEquals(PRESHARED_KEY, sktUri.getPresharedKey());
|
|
||||||
assertEquals(SSID, sktUri.getWifiSsid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBackAndForth() throws URISyntaxException {
|
|
||||||
SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, null);
|
|
||||||
String encodedSktUri = sktUri.toUriString();
|
|
||||||
SktUri decodedSktUri = SktUri.parse(encodedSktUri);
|
|
||||||
|
|
||||||
assertEquals(sktUri, decodedSktUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBackAndForthWithSsid() throws URISyntaxException {
|
|
||||||
SktUri sktUri = SktUri.create(HOST, PORT, PRESHARED_KEY, SSID);
|
|
||||||
String encodedSktUri = sktUri.toUriString();
|
|
||||||
SktUri decodedSktUri = SktUri.parse(encodedSktUri);
|
|
||||||
|
|
||||||
assertEquals(sktUri, decodedSktUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = URISyntaxException.class)
|
|
||||||
public void testParse_withBadScheme_shouldFail() throws URISyntaxException {
|
|
||||||
SktUri.parse(String.format("XXXGPSKT:%s/%d/%s/SSID:%s",
|
|
||||||
HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = URISyntaxException.class)
|
|
||||||
public void testParse_withBadPsk_shouldFail() throws URISyntaxException {
|
|
||||||
SktUri.parse(String.format("OPGPSKT:%s/%d/xx%s/SSID:%s",
|
|
||||||
HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = URISyntaxException.class)
|
|
||||||
public void testParse_withBadPort_shouldFail() throws URISyntaxException {
|
|
||||||
SktUri.parse(String.format("OPGPSKT:%s/x%d/%s/SSID:%s",
|
|
||||||
HOST, PORT, Hex.toHexString(PRESHARED_KEY), Hex.toHexString(SSID.getBytes())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user