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:
Vincent Breitmoser 2021-01-29 12:09:37 +01:00
parent 2cc35ce970
commit 5eaa7518e8
17 changed files with 2 additions and 2324 deletions

View file

@ -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]);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}
}

View file

@ -21,19 +21,16 @@ package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
import org.sufficientlysecure.keychain.util.Preferences;
import timber.log.Timber;
@ -48,7 +45,6 @@ public class CreateKeyStartFragment extends Fragment {
View mImportKey;
View mSecurityToken;
TextView mSkipOrCancel;
View mSecureDeviceSetup;
/**
@ -72,7 +68,6 @@ public class CreateKeyStartFragment extends Fragment {
mImportKey = view.findViewById(R.id.create_key_import_button);
mSecurityToken = view.findViewById(R.id.create_key_security_token_button);
mSkipOrCancel = view.findViewById(R.id.create_key_cancel);
mSecureDeviceSetup = view.findViewById(R.id.create_key_secure_device_setup);
if (mCreateKeyActivity.mFirstTime) {
mSkipOrCancel.setText(R.string.first_time_skip);
@ -96,15 +91,6 @@ public class CreateKeyStartFragment extends Fragment {
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 -> {
if (!mCreateKeyActivity.mFirstTime) {
mCreateKeyActivity.setResult(Activity.RESULT_CANCELED);

View file

@ -40,7 +40,6 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
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.util.IntentIntegratorSupportV4;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
@ -155,17 +154,6 @@ public class ImportKeysProxyActivity extends FragmentActivity
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
if (uri == null || uri.getScheme() == null ||
!uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {

View file

@ -19,9 +19,6 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.View;
@ -41,8 +38,6 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferFragment;
import org.sufficientlysecure.keychain.ui.transfer.view.TransferNotAvailableFragment;
import org.sufficientlysecure.keychain.util.FabContainer;
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_APPS = 3;
static final int ID_BACKUP = 4;
public static final int ID_TRANSFER = 5;
static final int ID_SETTINGS = 6;
static final int ID_HELP = 7;
@ -85,11 +79,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
.withIdentifier(ID_APPS).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore)
.withIdentifier(ID_BACKUP).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.nav_transfer)
.withIcon(R.drawable.ic_wifi_lock_24dp)
.withIconColorRes(R.color.md_grey_600)
.withIconTintingEnabled(true)
.withIdentifier(ID_TRANSFER).withSelectable(false),
new DividerDrawerItem(),
new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false),
new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false)
@ -113,9 +102,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_BACKUP:
onBackupSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
case ID_SETTINGS:
intent = new Intent(MainActivity.this, SettingsActivity.class);
break;
@ -168,9 +154,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_APPS:
onAppsSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
}
}
@ -190,9 +173,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
case ID_APPS:
onAppsSelected();
break;
case ID_TRANSFER:
onTransferSelected();
break;
}
}
}
@ -234,18 +214,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai
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
protected void onSaveInstanceState(Bundle outState) {
// add the values which need to be saved from the drawer to the bundle

View file

@ -299,13 +299,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
startPassphraseActivity(REQUEST_BACKUP);
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: {
deleteKey();
return true;
@ -335,7 +328,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity {
}
MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup);
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);
changePassword.setVisible(unifiedKeyInfo.has_any_secret());

View file

@ -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();
}
}

View file

@ -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.
*