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.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.NoRouteToHostException;
import java.net.Socket; import java.net.Socket;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -36,11 +38,9 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import android.net.PskKeyManager; import android.net.PskKeyManager;
import android.net.Uri;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@@ -56,7 +56,6 @@ import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log; 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_SEND_OK = 3;
private static final int CONNECTION_RECEIVE_OK = 4; private static final int CONNECTION_RECEIVE_OK = 4;
private static final int CONNECTION_LOST = 5; private static final int CONNECTION_LOST = 5;
private static final int CONNECTION_ERROR_CONNECT = 6; private static final int CONNECTION_ERROR_NO_ROUTE_TO_HOST = 6;
private static final int CONNECTION_ERROR_WHILE_CONNECTED = 7; private static final int CONNECTION_ERROR_CONNECT = 7;
private static final int CONNECTION_ERROR_LISTEN = 8; 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_CONNECTING = 1500;
private static final int TIMEOUT_RECEIVING = 2000; private static final int TIMEOUT_RECEIVING = 2000;
private static final int TIMEOUT_WAITING = 500; private static final int TIMEOUT_WAITING = 500;
@@ -100,20 +99,18 @@ public class KeyTransferInteractor {
this.delimiterEnd = delimiterEnd; this.delimiterEnd = delimiterEnd;
} }
public void connectToServer(String connectionDetails, KeyTransferCallback callback) { public void connectToServer(String qrCodeContent, KeyTransferCallback callback) throws URISyntaxException {
Uri uri = Uri.parse(connectionDetails); SktUri sktUri = SktUri.parse(qrCodeContent);
final byte[] presharedKey = Hex.decode(uri.getUserInfo());
final String host = uri.getHost();
final int port = uri.getPort();
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(); transferThread.start();
} }
public void startServer(KeyTransferCallback callback) { public void startServer(KeyTransferCallback callback, String wifiSsid) {
byte[] presharedKey = generatePresharedKey(); byte[] presharedKey = generatePresharedKey();
transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey); transferThread = TransferThread.createServerTransferThread(delimiterStart, delimiterEnd, callback, presharedKey, wifiSsid);
transferThread.start(); transferThread.start();
} }
@@ -126,6 +123,7 @@ public class KeyTransferInteractor {
private final boolean isServer; private final boolean isServer;
private final String clientHost; private final String clientHost;
private final Integer clientPort; private final Integer clientPort;
private final String wifiSsid;
private KeyTransferCallback callback; private KeyTransferCallback callback;
private SSLServerSocket serverSocket; private SSLServerSocket serverSocket;
@@ -133,18 +131,18 @@ public class KeyTransferInteractor {
private String sendPassthrough; private String sendPassthrough;
static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd, static TransferThread createClientTransferThread(String delimiterStart, String delimiterEnd,
KeyTransferCallback callback, byte[] presharedKey, String host, int port) { KeyTransferCallback callback, byte[] presharedKey, String host, int port, String wifiSsid) {
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port); return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, false, host, port, wifiSsid);
} }
static TransferThread createServerTransferThread(String delimiterStart, String delimiterEnd, static TransferThread createServerTransferThread(String delimiterStart, String delimiterEnd,
KeyTransferCallback callback, byte[] presharedKey) { KeyTransferCallback callback, byte[] presharedKey, String wifiSsid) {
return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null); return new TransferThread(delimiterStart, delimiterEnd, callback, presharedKey, true, null, null, wifiSsid);
} }
private TransferThread(String delimiterStart, String delimiterEnd, private TransferThread(String delimiterStart, String delimiterEnd,
KeyTransferCallback callback, byte[] presharedKey, boolean isServer, KeyTransferCallback callback, byte[] presharedKey, boolean isServer,
String clientHost, Integer clientPort) { String clientHost, Integer clientPort, String wifiSsid) {
super("TLS-PSK Key Transfer Thread"); super("TLS-PSK Key Transfer Thread");
this.delimiterStart = delimiterStart; this.delimiterStart = delimiterStart;
@@ -154,6 +152,7 @@ public class KeyTransferInteractor {
this.presharedKey = presharedKey; this.presharedKey = presharedKey;
this.clientHost = clientHost; this.clientHost = clientHost;
this.clientPort = clientPort; this.clientPort = clientPort;
this.wifiSsid = wifiSsid;
this.isServer = isServer; this.isServer = isServer;
handler = new Handler(Looper.getMainLooper()); handler = new Handler(Looper.getMainLooper());
@@ -196,11 +195,8 @@ public class KeyTransferInteractor {
String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES); String[] enabledCipherSuites = intersectArrays(supportedCipherSuites, ALLOWED_CIPHERSUITES);
serverSocket.setEnabledCipherSuites(enabledCipherSuites); serverSocket.setEnabledCipherSuites(enabledCipherSuites);
String presharedKeyEncoded = Hex.toHexString(presharedKey); SktUri sktUri = SktUri.create(getIPAddress(true), serverSocket.getLocalPort(), presharedKey, wifiSsid);
String qrCodeData = String.format( invokeListener(CONNECTION_LISTENING, sktUri.toUriString());
QRCODE_URI_FORMAT, presharedKeyEncoded, getIPAddress(true), serverSocket.getLocalPort());
qrCodeData = qrCodeData.toUpperCase(Locale.getDefault());
invokeListener(CONNECTION_LISTENING, qrCodeData);
socket = serverSocket.accept(); socket = serverSocket.accept();
} catch (IOException e) { } catch (IOException e) {
@@ -219,7 +215,11 @@ public class KeyTransferInteractor {
socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING); socket.connect(new InetSocketAddress(InetAddress.getByName(clientHost), clientPort), TIMEOUT_CONNECTING);
} catch (IOException e) { } catch (IOException e) {
Log.e(Constants.TAG, "error while connecting!", 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; return null;
} }
} }
@@ -345,6 +345,9 @@ public class KeyTransferInteractor {
case CONNECTION_ERROR_WHILE_CONNECTED: case CONNECTION_ERROR_WHILE_CONNECTED:
callback.onConnectionError(arg); callback.onConnectionError(arg);
break; break;
case CONNECTION_ERROR_NO_ROUTE_TO_HOST:
callback.onConnectionErrorNoRouteToHost(wifiSsid);
break;
case CONNECTION_ERROR_CONNECT: case CONNECTION_ERROR_CONNECT:
callback.onConnectionErrorConnect(); callback.onConnectionErrorConnect();
break; break;
@@ -398,6 +401,7 @@ public class KeyTransferInteractor {
void onDataSentOk(String passthrough); void onDataSentOk(String passthrough);
void onConnectionErrorConnect(); void onConnectionErrorConnect();
void onConnectionErrorNoRouteToHost(String wifiSsid);
void onConnectionErrorListen(); void onConnectionErrorListen();
void onConnectionError(String arg); 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.io.IOException;
import java.net.URISyntaxException;
import java.util.List; import java.util.List;
import android.content.Context; import android.content.Context;
@@ -298,6 +299,13 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
resetAndStartListen(); resetAndStartListen();
} }
@Override
public void onConnectionErrorNoRouteToHost(String wifiSsid) {
view.showErrorConnectionFailed();
resetAndStartListen();
}
@Override @Override
public void onConnectionErrorListen() { public void onConnectionErrorListen() {
view.showErrorListenFailed(); view.showErrorListenFailed();
@@ -320,7 +328,11 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
view.showEstablishingConnection(); view.showEstablishingConnection();
keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); 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() { private void checkWifiResetAndStartListen() {
@@ -340,7 +352,7 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks<L
connectionClear(); connectionClear();
keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END);
keyTransferServerInteractor.startServer(this); keyTransferServerInteractor.startServer(this, null);
view.showWaitingForConnection(); view.showWaitingForConnection();
view.setShowDoneIcon(false); view.setShowDoneIcon(false);

View File

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