tls-psk: extract skt uri handling, and use new qr code format
This commit is contained in:
parent
e5189e0c39
commit
b92778f6e9
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue