tls-psk: extract skt uri handling, and use new qr code format

This commit is contained in:
Vincent Breitmoser 2017-06-17 00:23:23 +02:00
parent e5189e0c39
commit b92778f6e9
5 changed files with 244 additions and 36 deletions

View file

@ -27,8 +27,10 @@ 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.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@ -36,11 +38,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import android.net.PskKeyManager;
import android.net.Uri;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.Looper;
@ -56,7 +56,6 @@ import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
@ -78,11 +77,11 @@ public class KeyTransferInteractor {
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_CONNECT = 6;
private static final int CONNECTION_ERROR_WHILE_CONNECTED = 7;
private static final int CONNECTION_ERROR_LISTEN = 8;
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 String QRCODE_URI_FORMAT = "PGP+TRANSFER://%s@%s:%s";
private static final int TIMEOUT_CONNECTING = 1500;
private static final int TIMEOUT_RECEIVING = 2000;
private static final int TIMEOUT_WAITING = 500;
@ -100,20 +99,18 @@ public class KeyTransferInteractor {
this.delimiterEnd = delimiterEnd;
}
public void connectToServer(String connectionDetails, KeyTransferCallback callback) {
Uri uri = Uri.parse(connectionDetails);
final byte[] presharedKey = Hex.decode(uri.getUserInfo());
final String host = uri.getHost();
final int port = uri.getPort();
public void connectToServer(String qrCodeContent, KeyTransferCallback callback) throws URISyntaxException {
SktUri sktUri = SktUri.parse(qrCodeContent);
transferThread = TransferThread.createClientTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, host, port);
transferThread = TransferThread.createClientTransferThread(delimiterStart, delimiterEnd, callback,
sktUri.getPresharedKey(), sktUri.getHost(), sktUri.getPort(), sktUri.getWifiSsid());
transferThread.start();
}
public void startServer(KeyTransferCallback callback) {
public void startServer(KeyTransferCallback callback, String wifiSsid) {
byte[] presharedKey = generatePresharedKey();
transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey);
transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, wifiSsid);
transferThread.start();
}
@ -126,6 +123,7 @@ public class KeyTransferInteractor {
private final boolean isServer;
private final String clientHost;
private final Integer clientPort;
private final String wifiSsid;
private KeyTransferCallback callback;
private SSLServerSocket serverSocket;
@ -133,18 +131,18 @@ public class KeyTransferInteractor {
private String sendPassthrough;
static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd,
KeyTransferCallback callback, byte[] presharedKey, String host, int port) {
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port);
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) {
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null);
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 clientHost, Integer clientPort, String wifiSsid) {
super("TLS-PSK Key Transfer Thread");
this.delimiterStart = delimiterStart;
@ -154,6 +152,7 @@ public class KeyTransferInteractor {
this.presharedKey = presharedKey;
this.clientHost = clientHost;
this.clientPort = clientPort;
this.wifiSsid = wifiSsid;
this.isServer = isServer;
handler = new Handler(Looper.getMainLooper());
@ -196,11 +195,8 @@ public class KeyTransferInteractor {
String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
serverSocket.setEnabledCipherSuites(enabledCipherSuites);
String presharedKeyEncoded = Hex.toHexString(presharedKey);
String qrCodeData = String.format(
QRCODE_URI_FORMAT, presharedKeyEncoded, getIPAddress(true), serverSocket.getLocalPort());
qrCodeData = qrCodeData.toUpperCase(Locale.getDefault());
invokeListener(CONNECTION_LISTENING, qrCodeData);
SktUri sktUri = SktUri.create(getIPAddress(true), serverSocket.getLocalPort(), presharedKey, wifiSsid);
invokeListener(CONNECTION_LISTENING, sktUri.toUriString());
socket = serverSocket.accept();
} catch (IOException e) {
@ -219,7 +215,11 @@ public class KeyTransferInteractor {
socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING);
} catch (IOException e) {
Log.e(Constants.TAG, "error while connecting!", e);
invokeListener(CONNECTION_ERROR_CONNECT, null);
if (e instanceof NoRouteToHostException) {
invokeListener(CONNECTION_ERROR_NO_ROUTE_TO_HOST, wifiSsid);
} else {
invokeListener(CONNECTION_ERROR_CONNECT, null);
}
return null;
}
}
@ -345,6 +345,9 @@ public class KeyTransferInteractor {
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;
@ -398,6 +401,7 @@ public class KeyTransferInteractor {
void onDataSentOk(String passthrough);
void onConnectionErrorConnect();
void onConnectionErrorNoRouteToHost(String wifiSsid);
void onConnectionErrorListen();
void onConnectionError(String arg);
}

View file

@ -0,0 +1,91 @@
package org.sufficientlysecure.keychain.network;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.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 org.sufficientlysecure.keychain.util.Log;
@AutoValue
abstract class SktUri {
private static final String SKT_SCHEME = "OPGPSKT";
private static final String QRCODE_URI_FORMAT = SKT_SCHEME + ":%s/%d/%s";
private static final String QRCODE_URI_FORMAT_SSID = SKT_SCHEME + ":%s/%d/%s/SSID:%s";
public abstract String getHost();
public abstract int getPort();
public abstract byte[] getPresharedKey();
@Nullable
public abstract String getWifiSsid();
@NonNull
public static SktUri parse(String input) throws URISyntaxException {
if (!input.startsWith(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) {
Log.d(Constants.TAG, "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

@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui.transfer.presenter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import android.content.Context;
@ -298,6 +299,13 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
resetAndStartListen();
}
@Override
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
view.showErrorConnectionFailed();
resetAndStartListen();
}
@Override
public void onConnectionErrorListen() {
view.showErrorListenFailed();
@ -320,7 +328,11 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
view.showEstablishingConnection();
keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this);
try {
keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this);
} catch (URISyntaxException e) {
view.showErrorConnectionFailed();
}
}
private void checkWifiResetAndStartListen() {
@ -340,7 +352,7 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
connectionClear();
keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
keyTransferServerInteractor.startServer(this);
keyTransferServerInteractor.startServer(this, null);
view.showWaitingForConnection();
view.setShowDoneIcon(false);

View file

@ -1,19 +1,14 @@
package org.sufficientlysecure.keychain.network;
import java.security.Security;
import java.net.URISyntaxException;
import android.os.Build.VERSION_CODES;
import android.support.annotation.RequiresApi;
import junit.framework.Assert;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowLooper;
import org.sufficientlysecure.keychain.KeychainTestRunner;
import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback;
import static junit.framework.Assert.assertTrue;
@ -38,7 +33,7 @@ public class KeyTransferInteractorTest {
}
// @Test
public void testServerShouldGiveSuccessCallback() {
public void testServerShouldGiveSuccessCallback() throws URISyntaxException {
KeyTransferInteractor serverKeyTransferInteractor = new KeyTransferInteractor(DELIM_START, DELIM_END);
serverKeyTransferInteractor.startServer(new SimpleKeyTransferCallback() {
@ -51,7 +46,7 @@ public class KeyTransferInteractorTest {
public void onConnectionEstablished(String otherName) {
serverConnectionEstablished = true;
}
});
}, null);
waitForLooperCallback();
Assert.assertNotNull(receivedQrCodeData);
@ -103,6 +98,11 @@ public class KeyTransferInteractorTest {
fail("unexpected callback: onDataSentOk");
}
@Override
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
fail("unexpected callback: onConnectionErrorNoRouteToHost");
}
@Override
public void onConnectionErrorConnect() {
fail("unexpected callback: onConnectionErrorConnect");

View file

@ -0,0 +1,101 @@
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())));
}
}