Enable TLS connection and SAS verification between device transfer server and client.

fork-5.53.8
Cody Henthorne 2021-03-11 13:16:51 -05:00
rodzic c25250cb05
commit e74460bd91
23 zmienionych plików z 1376 dodań i 354 usunięć

Wyświetl plik

@ -17,6 +17,25 @@ android {
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard.cfg'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard.cfg'
}
}
packagingOptions {
exclude '/org/spongycastle/x509/CertPathReviewerMessages.properties'
exclude '/org/spongycastle/x509/CertPathReviewerMessages_de.properties'
}
}
dependencies {

Wyświetl plik

@ -0,0 +1,7 @@
-dontoptimize
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.signal.devicetransfer.** { *; }
-keepclassmembers class ** {
public void onEvent*(**);
}

Wyświetl plik

@ -16,6 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationManagerCompat;
@ -26,7 +27,7 @@ import org.signal.devicetransfer.ClientTask;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.DeviceToDeviceTransferService.TransferNotificationData;
import org.signal.devicetransfer.ServerTask;
import org.signal.devicetransfer.TransferMode;
import org.signal.devicetransfer.TransferStatus;
import java.io.IOException;
import java.io.InputStream;
@ -57,7 +58,6 @@ public class MainActivity extends AppCompatActivity {
findViewById(R.id.start_server).setOnClickListener(v -> {
DeviceToDeviceTransferService.startServer(this,
8888,
new ServerReceiveRandomBytes(),
data,
PendingIntent.getActivity(this,
@ -70,7 +70,6 @@ public class MainActivity extends AppCompatActivity {
findViewById(R.id.start_client).setOnClickListener(v -> {
DeviceToDeviceTransferService.startClient(this,
8888,
new ClientSendRandomBytes(),
data,
PendingIntent.getActivity(this,
@ -81,9 +80,7 @@ public class MainActivity extends AppCompatActivity {
list.removeAllViews();
});
findViewById(R.id.stop).setOnClickListener(v -> {
DeviceToDeviceTransferService.stop(this);
});
findViewById(R.id.stop).setOnClickListener(v -> DeviceToDeviceTransferService.stop(this));
findViewById(R.id.enable_permission).setOnClickListener(v -> {
if (Build.VERSION.SDK_INT >= 23 && checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
@ -95,19 +92,27 @@ public class MainActivity extends AppCompatActivity {
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull TransferMode event) {
public void onEventMainThread(@NonNull TransferStatus event) {
TextView text = new TextView(this);
text.setText(event.toString());
text.setText(event.getTransferMode().toString());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
text.setLayoutParams(params);
list.addView(text);
if (event.getTransferMode() == TransferStatus.TransferMode.VERIFICATION_REQUIRED) {
new AlertDialog.Builder(this).setTitle("Verification Required")
.setMessage("Code: " + ((TransferStatus.VerificationTransferStatus) event).getAuthenticationCode())
.setPositiveButton("Yes, Same", (d, w) -> DeviceToDeviceTransferService.setAuthenticationCodeVerified(this, true))
.setNegativeButton("No, different", (d, w) -> DeviceToDeviceTransferService.setAuthenticationCodeVerified(this, false))
.setCancelable(false)
.show();
}
}
private static class ClientSendRandomBytes implements ClientTask {
private static final String TAG = "ClientSend";
private final int rounds = 1000;
private static final String TAG = "ClientSend";
private static final int ROUNDS = 131072 / 4; // Use 131072 to send 1GB
@Override
public void run(@NonNull Context context, @NonNull OutputStream outputStream) throws IOException {
@ -116,14 +121,18 @@ public class MainActivity extends AppCompatActivity {
r.nextBytes(data);
long start = System.currentTimeMillis();
Log.i(TAG, "Sending " + ((data.length * rounds) / 1024 / 1024) + "MB of random data!!!");
for (int i = 0; i < rounds; i++) {
Log.i(TAG, "Sending " + ((data.length * ROUNDS) / 1024 / 1024) + "MB of random data!!!");
for (int i = 0; i < ROUNDS; i++) {
outputStream.write(data);
outputStream.flush();
}
long end = System.currentTimeMillis();
Log.i(TAG, "Sending took: " + (end - start));
}
@Override
public void success() {
}
}
private static class ServerReceiveRandomBytes implements ServerTask {

Wyświetl plik

@ -2,6 +2,10 @@ apply plugin: 'com.android.library'
apply plugin: 'witness'
apply from: 'witness-verifications.gradle'
repositories {
mavenCentral()
}
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
@ -9,6 +13,7 @@ android {
defaultConfig {
minSdkVersion MINIMUM_SDK
targetSdkVersion TARGET_SDK
consumerProguardFiles 'lib-proguard-rules.pro'
}
compileOptions {
@ -23,8 +28,18 @@ dependencyVerification {
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation project(':core-util')
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation 'com.madgag.spongycastle:pkix:1.54.0.0'
implementation 'com.madgag.spongycastle:pg:1.54.0.0'
api 'org.greenrobot:eventbus:3.0.0'
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test:core:1.2.0'
testImplementation ('org.robolectric:robolectric:4.4') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation 'org.robolectric:shadows-multidex:4.4'
testImplementation 'org.hamcrest:hamcrest:2.2'
}

Wyświetl plik

@ -0,0 +1,8 @@
-keep class org.spongycastle.jcajce.provider.digest.SHA256** {*;}
-keepclassmembers class org.spongycastle.jcajce.provider.digest.SHA256** {*;}
-keep class org.spongycastle.jcajce.provider.asymmetric.RSA**
-keepclassmembers class org.spongycastle.jcajce.provider.asymmetric.RSA** {*;}
-keep class org.spongycastle.jcajce.provider.asymmetric.rsa.DigestSignatureSpi** {*;}
-keepclassmembers class org.spongycastle.jcajce.provider.asymmetric.rsa.DigestSignatureSpi** {*;}

Wyświetl plik

@ -19,4 +19,9 @@ public interface ClientTask extends Serializable {
* @param outputStream Output stream associated with socket connected to remote server.
*/
void run(@NonNull Context context, @NonNull OutputStream outputStream) throws IOException;
/**
* Called after the output stream has been successfully flushed and closed.
*/
void success();
}

Wyświetl plik

@ -29,16 +29,15 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
private static final String TAG = Log.tag(DeviceToDeviceTransferService.class);
private static final int INVALID_PORT = -1;
private static final String ACTION_START_SERVER = "start";
private static final String ACTION_START_CLIENT = "start_client";
private static final String ACTION_SET_VERIFIED = "set_verified";
private static final String ACTION_STOP = "stop";
private static final String EXTRA_PENDING_INTENT = "extra_pending_intent";
private static final String EXTRA_TASK = "extra_task";
private static final String EXTRA_NOTIFICATION = "extra_notification_data";
private static final String EXTRA_PORT = "extra_port";
private static final String EXTRA_IS_VERIFIED = "is_verified";
private TransferNotificationData notificationData;
private PendingIntent pendingIntent;
@ -46,7 +45,6 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
private DeviceTransferClient client;
public static void startServer(@NonNull Context context,
int port,
@NonNull ServerTask serverTask,
@NonNull TransferNotificationData transferNotificationData,
@Nullable PendingIntent pendingIntent)
@ -54,7 +52,6 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
Intent intent = new Intent(context, DeviceToDeviceTransferService.class);
intent.setAction(ACTION_START_SERVER)
.putExtra(EXTRA_TASK, serverTask)
.putExtra(EXTRA_PORT, port)
.putExtra(EXTRA_NOTIFICATION, transferNotificationData)
.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
@ -62,7 +59,6 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
}
public static void startClient(@NonNull Context context,
int port,
@NonNull ClientTask clientTask,
@NonNull TransferNotificationData transferNotificationData,
@Nullable PendingIntent pendingIntent)
@ -70,13 +66,20 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
Intent intent = new Intent(context, DeviceToDeviceTransferService.class);
intent.setAction(ACTION_START_CLIENT)
.putExtra(EXTRA_TASK, clientTask)
.putExtra(EXTRA_PORT, port)
.putExtra(EXTRA_NOTIFICATION, transferNotificationData)
.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
context.startService(intent);
}
public static void setAuthenticationCodeVerified(@NonNull Context context, boolean verified) {
Intent intent = new Intent(context, DeviceToDeviceTransferService.class);
intent.setAction(ACTION_SET_VERIFIED)
.putExtra(EXTRA_IS_VERIFIED, verified);
context.startService(intent);
}
public static void stop(@NonNull Context context) {
context.startService(new Intent(context, DeviceToDeviceTransferService.class).setAction(ACTION_STOP));
}
@ -90,14 +93,10 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull TransferMode event) {
public void onEventMainThread(@NonNull TransferStatus event) {
updateNotification(event);
}
private void update(@NonNull TransferMode transferMode) {
EventBus.getDefault().postSticky(transferMode);
}
@Override
public void onDestroy() {
Log.e(TAG, "onDestroy");
@ -105,12 +104,12 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
EventBus.getDefault().unregister(this);
if (client != null) {
client.shutdown();
client.stop();
client = null;
}
if (server != null) {
server.shutdown();
server.stop();
server = null;
}
@ -130,45 +129,46 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
final WifiDirect.AvailableStatus availability = WifiDirect.getAvailability(this);
if (availability != WifiDirect.AvailableStatus.AVAILABLE) {
update(availability == WifiDirect.AvailableStatus.FINE_LOCATION_PERMISSION_NOT_GRANTED ? TransferMode.PERMISSIONS
: TransferMode.UNAVAILABLE);
shutdown();
return START_NOT_STICKY;
}
Log.d(TAG, "Action: " + action);
switch (action) {
case ACTION_START_SERVER: {
int port = intent.getIntExtra(EXTRA_PORT, INVALID_PORT);
if (server == null && port != -1) {
if (server == null) {
notificationData = intent.getParcelableExtra(EXTRA_NOTIFICATION);
pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
server = new DeviceTransferServer(getApplicationContext(),
(ServerTask) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_TASK)),
port,
this);
updateNotification(TransferMode.READY);
server.start();
} else {
Log.i(TAG, "Can't start server. already_started: " + (server != null) + " port: " + port);
Log.i(TAG, "Can't start server, already started.");
}
break;
}
case ACTION_START_CLIENT: {
int port = intent.getIntExtra(EXTRA_PORT, INVALID_PORT);
if (client == null && port != -1) {
if (client == null) {
notificationData = intent.getParcelableExtra(EXTRA_NOTIFICATION);
pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
client = new DeviceTransferClient(getApplicationContext(),
(ClientTask) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_TASK)),
port,
this);
updateNotification(TransferMode.READY);
client.start();
} else {
Log.i(TAG, "Can't start client. already_started: " + (client != null) + " port: " + port);
Log.i(TAG, "Can't start client, client already started.");
}
break;
}
case ACTION_SET_VERIFIED:
boolean isVerified = intent.getBooleanExtra(EXTRA_IS_VERIFIED, false);
if (server != null) {
server.setVerified(isVerified);
} else if (client != null) {
client.setVerified(isVerified);
}
break;
case ACTION_STOP:
shutdown();
break;
@ -186,20 +186,20 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa
});
}
private void updateNotification(@NonNull TransferMode transferMode) {
private void updateNotification(@NonNull TransferStatus transferStatus) {
if (notificationData != null && (client != null || server != null)) {
startForeground(notificationData.notificationId, createNotification(transferMode, notificationData));
startForeground(notificationData.notificationId, createNotification(transferStatus, notificationData));
}
}
private @NonNull Notification createNotification(@NonNull TransferMode transferMode, @NonNull TransferNotificationData notificationData) {
private @NonNull Notification createNotification(@NonNull TransferStatus transferStatus, @NonNull TransferNotificationData notificationData) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationData.channelId);
//TODO [cody] build notification to spec
builder.setSmallIcon(notificationData.icon)
.setOngoing(true)
.setContentTitle("Device Transfer")
.setContentText("Status: " + transferMode.name())
.setContentText("Status: " + transferStatus.getTransferMode().name())
.setContentIntent(pendingIntent);
return builder.build();

Wyświetl plik

@ -0,0 +1,226 @@
package org.signal.devicetransfer;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import org.signal.core.util.StreamUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Allows two parties to authenticate each other via short authentication strings (SAS).
* <ol>
* <li>Client generates a random data, and then MAC(k=random data, m=certificate) to get a commitment.</li>
* <li>Client sends commitment to the server.</li>
* <li>Server stores commitment and generates it's own random data.</li>
* <li>Server sends it's random data to client.</li>
* <li>Client stores server random data and sends it's random data to the server.</li>
* <li>Server can then MAC(k=client random data, m=certificate) to verify the original commitment.</li>
* <li>Client and Server can compute a SAS using the two randoms.</li>
* </ol>
*/
final class DeviceTransferAuthentication {
public static final int DIGEST_LENGTH = 32;
private static final String MAC_ALGORITHM = "HmacSHA256";
private DeviceTransferAuthentication() {}
/**
* Perform the client side of the SAS generation via input and output streams.
*
* @param certificate x509 certificate of the TLS connection
* @param inputStream stream to read data from the {@link Server}
* @param outputStream stream to write data to the {@link Server}
* @return Computed SAS
* @throws DeviceTransferAuthenticationException When something in the code generation fails
* @throws IOException When a communication issue occurs over one of the two streams
*/
public static int generateClientAuthenticationCode(@NonNull byte[] certificate,
@NonNull InputStream inputStream,
@NonNull OutputStream outputStream)
throws DeviceTransferAuthenticationException, IOException
{
Client authentication = new Client(certificate);
outputStream.write(authentication.getCommitment());
outputStream.flush();
byte[] serverRandom = new byte[DeviceTransferAuthentication.DIGEST_LENGTH];
StreamUtil.readFully(inputStream, serverRandom, serverRandom.length);
byte[] clientRandom = authentication.setServerRandomAndGetClientRandom(serverRandom);
outputStream.write(clientRandom);
outputStream.flush();
return authentication.computeShortAuthenticationCode();
}
/**
* Perform the server side of the SAS generation via input and output streams.
*
* @param certificate x509 certificate of the TLS connection
* @param inputStream stream to read data from the {@link Client}
* @param outputStream stream to write data to the {@link Client}
* @return Computed SAS
* @throws DeviceTransferAuthenticationException When something in the code generation fails or the client
* provided commitment does not match the computed version
* @throws IOException When a communication issue occurs over one of the two streams
*/
public static int generateServerAuthenticationCode(@NonNull byte[] certificate,
@NonNull InputStream inputStream,
@NonNull OutputStream outputStream)
throws DeviceTransferAuthenticationException, IOException
{
byte[] clientCommitment = new byte[DeviceTransferAuthentication.DIGEST_LENGTH];
StreamUtil.readFully(inputStream, clientCommitment, clientCommitment.length);
DeviceTransferAuthentication.Server authentication = new DeviceTransferAuthentication.Server(certificate, clientCommitment);
outputStream.write(authentication.getRandom());
outputStream.flush();
byte[] clientRandom = new byte[DeviceTransferAuthentication.DIGEST_LENGTH];
StreamUtil.readFully(inputStream, clientRandom, clientRandom.length);
authentication.setClientRandom(clientRandom);
return authentication.computeShortAuthenticationCode();
}
private static @NonNull Mac getMac(@NonNull byte[] secret) throws DeviceTransferAuthenticationException {
try {
Mac mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(new SecretKeySpec(secret, MAC_ALGORITHM));
return mac;
} catch (Exception e) {
throw new DeviceTransferAuthenticationException(e);
}
}
private static int computeShortAuthenticationCode(@NonNull byte[] clientRandom,
@NonNull byte[] serverRandom)
throws DeviceTransferAuthenticationException
{
byte[] authentication = getMac(clientRandom).doFinal(serverRandom);
ByteBuffer buffer = ByteBuffer.wrap(authentication);
buffer.order(ByteOrder.BIG_ENDIAN);
return buffer.getInt(authentication.length - 4) & 0x007f_ffff;
}
private static @NonNull byte[] copyOf(@NonNull byte[] input) {
return Arrays.copyOf(input, input.length);
}
private static void validateLength(@NonNull byte[] input) throws DeviceTransferAuthenticationException {
if (input.length != DIGEST_LENGTH) {
throw new DeviceTransferAuthenticationException("invalid digest length");
}
}
/**
* Server side of authentication, responsible for verifying connecting
* devices commitment and generating a code.
*/
@VisibleForTesting
static final class Server {
private final byte[] random;
private final byte[] certificate;
private final byte[] clientCommitment;
private byte[] clientRandom;
public Server(@NonNull byte[] certificate, @NonNull byte[] clientCommitment) throws DeviceTransferAuthenticationException {
validateLength(clientCommitment);
this.certificate = copyOf(certificate);
this.clientCommitment = copyOf(clientCommitment);
SecureRandom secureRandom = new SecureRandom();
this.random = new byte[DIGEST_LENGTH];
secureRandom.nextBytes(this.random);
}
public @NonNull byte[] getRandom() {
return copyOf(random);
}
public void setClientRandom(@NonNull byte[] clientRandom) throws DeviceTransferAuthenticationException {
validateLength(clientRandom);
this.clientRandom = copyOf(clientRandom);
}
public void verifyClientRandom() throws DeviceTransferAuthenticationException {
if (clientRandom == null) {
throw new DeviceTransferAuthenticationException("no client random set");
}
byte[] computedCommitment = getMac(copyOf(clientRandom)).doFinal(copyOf(certificate));
boolean commitmentsMatch = MessageDigest.isEqual(clientCommitment, computedCommitment);
if (!commitmentsMatch) {
throw new DeviceTransferAuthenticationException("commitments do not match, do not proceed");
}
}
public int computeShortAuthenticationCode() throws DeviceTransferAuthenticationException {
verifyClientRandom();
return DeviceTransferAuthentication.computeShortAuthenticationCode(copyOf(clientRandom), copyOf(random));
}
}
/**
* Client side of authentication, responsible for starting authentication with server.
*/
@VisibleForTesting
static final class Client {
private final byte[] random;
private final byte[] commitment;
private byte[] serverRandom;
public Client(@NonNull byte[] certificate) throws DeviceTransferAuthenticationException {
SecureRandom secureRandom = new SecureRandom();
this.random = new byte[DIGEST_LENGTH];
secureRandom.nextBytes(this.random);
commitment = getMac(copyOf(this.random)).doFinal(copyOf(certificate));
}
public @NonNull byte[] getCommitment() {
return copyOf(commitment);
}
public @NonNull byte[] setServerRandomAndGetClientRandom(@NonNull byte[] serverRandom) throws DeviceTransferAuthenticationException {
validateLength(serverRandom);
this.serverRandom = copyOf(serverRandom);
return copyOf(random);
}
public int computeShortAuthenticationCode() throws DeviceTransferAuthenticationException {
if (serverRandom == null) {
throw new DeviceTransferAuthenticationException("no server random set");
}
return DeviceTransferAuthentication.computeShortAuthenticationCode(copyOf(random), copyOf(serverRandom));
}
}
public static final class DeviceTransferAuthenticationException extends Exception {
public DeviceTransferAuthenticationException(@NonNull String message) {
super(message);
}
public DeviceTransferAuthenticationException(@NonNull Throwable cause) {
super(cause);
}
}
}

Wyświetl plik

@ -12,13 +12,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
/**
@ -29,8 +25,8 @@ import java.util.concurrent.TimeUnit;
* problems. It will also retry the task if an issue occurs while running it.
* <p>
* The client is setup to retry indefinitely and will only bail on its own if it's
* unable to start {@link WifiDirect}. A call to {@link #shutdown()} is required to
* stop client from the "outside."
* unable to start {@link WifiDirect} or the network client connects and then completes
* or failed. A call to {@link #stop()} is required to stop client from the "outside."
* <p>
* Summary of mitigations:
* <ul>
@ -39,37 +35,43 @@ import java.util.concurrent.TimeUnit;
* <li>Retry connecting to the server until successful, disconnected from WiFi Direct network, or told to stop.</li>
* </ul>
*/
public final class DeviceTransferClient implements Handler.Callback {
final class DeviceTransferClient implements Handler.Callback {
private static final String TAG = Log.tag(DeviceTransferClient.class);
private static final int START_CLIENT = 0;
private static final int START_NETWORK_CLIENT = 1;
private static final int NETWORK_DISCONNECTED = 2;
private static final int CONNECT_TO_SERVICE = 3;
private static final int RESTART_CLIENT = 4;
private static final int START_IP_EXCHANGE = 5;
private static final int IP_EXCHANGE_SUCCESS = 6;
private static final String TAG = Log.tag(DeviceTransferClient.class);
private static final int START_CLIENT = 0;
private static final int STOP_CLIENT = 1;
private static final int START_NETWORK_CLIENT = 2;
private static final int NETWORK_DISCONNECTED = 3;
private static final int CONNECT_TO_SERVICE = 4;
private static final int RESTART_CLIENT = 5;
private static final int START_IP_EXCHANGE = 6;
private static final int IP_EXCHANGE_SUCCESS = 7;
private static final int SET_VERIFIED = 8;
private final Context context;
private final int port;
private int remotePort;
private HandlerThread commandAndControlThread;
private final Handler handler;
private final ClientTask clientTask;
private final ShutdownCallback shutdownCallback;
private WifiDirect wifiDirect;
private ClientThread clientThread;
private NetworkClientThread clientThread;
private final Runnable autoRestart;
private IpExchange.IpExchangeThread ipExchangeThread;
private static void update(@NonNull TransferMode transferMode) {
EventBus.getDefault().postSticky(transferMode);
private static void update(@NonNull TransferStatus transferStatus) {
Log.d(TAG, "transferStatus: " + transferStatus.getTransferMode().name());
EventBus.getDefault().postSticky(transferStatus);
}
@AnyThread
public DeviceTransferClient(@NonNull Context context, @NonNull ClientTask clientTask, int port, @Nullable ShutdownCallback shutdownCallback) {
public DeviceTransferClient(@NonNull Context context,
@NonNull ClientTask clientTask,
@Nullable ShutdownCallback shutdownCallback)
{
this.context = context;
this.clientTask = clientTask;
this.port = port;
this.shutdownCallback = shutdownCallback;
this.commandAndControlThread = SignalExecutors.getAndStartHandlerThread("client-cnc");
this.handler = new Handler(commandAndControlThread.getLooper(), this);
@ -80,12 +82,28 @@ public final class DeviceTransferClient implements Handler.Callback {
}
@AnyThread
public void start() {
handler.sendMessage(handler.obtainMessage(START_CLIENT));
public synchronized void start() {
if (commandAndControlThread != null) {
update(TransferStatus.ready());
handler.sendEmptyMessage(START_CLIENT);
}
}
@AnyThread
public synchronized void shutdown() {
public synchronized void stop() {
if (commandAndControlThread != null) {
handler.sendEmptyMessage(STOP_CLIENT);
}
}
@AnyThread
public synchronized void setVerified(boolean isVerified) {
if (commandAndControlThread != null) {
handler.sendMessage(handler.obtainMessage(SET_VERIFIED, isVerified));
}
}
private synchronized void shutdown() {
stopIpExchange();
stopClient();
stopWifiDirect();
@ -96,14 +114,27 @@ public final class DeviceTransferClient implements Handler.Callback {
commandAndControlThread.interrupt();
commandAndControlThread = null;
}
update(TransferStatus.shutdown());
}
private void internalShutdown() {
shutdown();
if (shutdownCallback != null) {
shutdownCallback.shutdown();
}
}
@Override
public boolean handleMessage(@NonNull Message message) {
Log.d(TAG, "Handle message: " + message.what);
switch (message.what) {
case START_CLIENT:
startWifiDirect();
break;
case STOP_CLIENT:
shutdown();
break;
case START_NETWORK_CLIENT:
startClient((String) message.obj);
break;
@ -111,7 +142,8 @@ public final class DeviceTransferClient implements Handler.Callback {
stopClient();
break;
case CONNECT_TO_SERVICE:
connectToService((String) message.obj);
stopServiceDiscovery();
connectToService((String) message.obj, message.arg1);
break;
case RESTART_CLIENT:
stopClient();
@ -124,12 +156,26 @@ public final class DeviceTransferClient implements Handler.Callback {
case IP_EXCHANGE_SUCCESS:
ipExchangeSuccessful((String) message.obj);
break;
default:
shutdown();
if (shutdownCallback != null) {
shutdownCallback.shutdown();
case SET_VERIFIED:
if (clientThread != null) {
clientThread.setVerified((Boolean) message.obj);
}
throw new AssertionError();
break;
case NetworkClientThread.NETWORK_CLIENT_SSL_ESTABLISHED:
update(TransferStatus.verificationRequired((Integer) message.obj));
break;
case NetworkClientThread.NETWORK_CLIENT_CONNECTED:
update(TransferStatus.serviceConnected());
break;
case NetworkClientThread.NETWORK_CLIENT_DISCONNECTED:
update(TransferStatus.networkConnected());
break;
case NetworkClientThread.NETWORK_CLIENT_STOPPED:
internalShutdown();
break;
default:
internalShutdown();
throw new AssertionError("Unknown message: " + message.what);
}
return false;
}
@ -140,25 +186,41 @@ public final class DeviceTransferClient implements Handler.Callback {
return;
}
update(TransferMode.STARTING_UP);
update(TransferStatus.startingUp());
try {
wifiDirect = new WifiDirect(context);
wifiDirect.initialize(new WifiDirectListener());
wifiDirect.discoverService();
Log.i(TAG, "Started service discovery, searching for service...");
update(TransferMode.DISCOVERY);
update(TransferStatus.discovery());
handler.postDelayed(autoRestart, TimeUnit.SECONDS.toMillis(15));
} catch (WifiDirectUnavailableException e) {
Log.e(TAG, e);
shutdown();
update(TransferMode.FAILED);
if (shutdownCallback != null) {
shutdownCallback.shutdown();
internalShutdown();
if (e.getReason() == WifiDirectUnavailableException.Reason.CHANNEL_INITIALIZATION ||
e.getReason() == WifiDirectUnavailableException.Reason.WIFI_P2P_MANAGER) {
update(TransferStatus.unavailable());
} else {
update(TransferStatus.failed());
}
}
}
private void stopServiceDiscovery() {
if (wifiDirect == null) {
return;
}
try {
Log.i(TAG, "Stopping service discovery");
wifiDirect.stopServiceDiscovery();
} catch (WifiDirectUnavailableException e) {
internalShutdown();
update(TransferStatus.failed());
}
}
private void stopWifiDirect() {
handler.removeCallbacks(autoRestart);
@ -166,7 +228,6 @@ public final class DeviceTransferClient implements Handler.Callback {
Log.i(TAG, "Shutting down WiFi Direct");
wifiDirect.shutdown();
wifiDirect = null;
update(TransferMode.READY);
}
}
@ -177,7 +238,11 @@ public final class DeviceTransferClient implements Handler.Callback {
}
Log.i(TAG, "Connection established, spinning up network client.");
clientThread = new ClientThread(context, clientTask, serverHostAddress, port);
clientThread = new NetworkClientThread(context,
clientTask,
serverHostAddress,
remotePort,
handler);
clientThread.start();
}
@ -185,11 +250,16 @@ public final class DeviceTransferClient implements Handler.Callback {
if (clientThread != null) {
Log.i(TAG, "Shutting down ClientThread");
clientThread.shutdown();
try {
clientThread.join(TimeUnit.SECONDS.toMillis(1));
} catch (InterruptedException e) {
Log.i(TAG, "Server thread took too long to shutdown", e);
}
clientThread = null;
}
}
private void connectToService(@NonNull String deviceAddress) {
private void connectToService(@NonNull String deviceAddress, int port) {
if (wifiDirect == null) {
Log.w(TAG, "WifiDirect is not initialized, we shouldn't be here.");
return;
@ -201,11 +271,17 @@ public final class DeviceTransferClient implements Handler.Callback {
while ((tries--) > 0) {
try {
wifiDirect.connect(deviceAddress);
update(TransferMode.NETWORK_CONNECTED);
update(TransferStatus.networkConnected());
remotePort = port;
return;
} catch (WifiDirectUnavailableException e) {
Log.w(TAG, "Unable to connect, tries: " + tries);
ThreadUtil.sleep(TimeUnit.SECONDS.toMillis(2));
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
} catch (InterruptedException ignored) {
Log.i(TAG, "Interrupted while connecting to service, bail now!");
return;
}
}
}
@ -213,12 +289,17 @@ public final class DeviceTransferClient implements Handler.Callback {
}
private void startIpExchange(@NonNull String groupOwnerHostAddress) {
ipExchangeThread = IpExchange.getIp(groupOwnerHostAddress, port, handler, IP_EXCHANGE_SUCCESS);
ipExchangeThread = IpExchange.getIp(groupOwnerHostAddress, remotePort, handler, IP_EXCHANGE_SUCCESS);
}
private void stopIpExchange() {
if (ipExchangeThread != null) {
ipExchangeThread.shutdown();
try {
ipExchangeThread.join(TimeUnit.SECONDS.toMillis(1));
} catch (InterruptedException e) {
Log.i(TAG, "IP Exchange thread took too long to shutdown", e);
}
ipExchangeThread = null;
}
}
@ -228,90 +309,11 @@ public final class DeviceTransferClient implements Handler.Callback {
handler.sendMessage(handler.obtainMessage(START_NETWORK_CLIENT, host));
}
private static class ClientThread extends Thread {
private volatile Socket client;
private volatile boolean isRunning;
private final Context context;
private final ClientTask clientTask;
private final String serverHostAddress;
private final int port;
public ClientThread(@NonNull Context context,
@NonNull ClientTask clientTask,
@NonNull String serverHostAddress,
int port)
{
this.context = context;
this.clientTask = clientTask;
this.serverHostAddress = serverHostAddress;
this.port = port;
}
@Override
public void run() {
Log.i(TAG, "Client thread running");
isRunning = true;
while (shouldKeepRunning()) {
Log.i(TAG, "Attempting to connect to server...");
try {
client = new Socket();
try {
client.bind(null);
client.connect(new InetSocketAddress(serverHostAddress, port), 10000);
DeviceTransferClient.update(TransferMode.SERVICE_CONNECTED);
clientTask.run(context, client.getOutputStream());
Log.i(TAG, "Done!!");
isRunning = false;
} catch (IOException e) {
Log.w(TAG, "Error connecting to server", e);
}
} catch (Exception e) {
Log.w(TAG, e);
} finally {
if (client != null && !client.isClosed()) {
try {
client.close();
} catch (IOException ignored) {}
}
DeviceTransferClient.update(TransferMode.NETWORK_CONNECTED);
}
if (shouldKeepRunning()) {
ThreadUtil.interruptableSleep(TimeUnit.SECONDS.toMillis(3));
}
}
Log.i(TAG, "Client exiting");
}
public void shutdown() {
isRunning = false;
try {
if (client != null) {
client.close();
}
} catch (IOException e) {
Log.w(TAG, "Error shutting down client socket", e);
}
interrupt();
}
private boolean shouldKeepRunning() {
return !isInterrupted() && isRunning;
}
}
public class WifiDirectListener implements WifiDirect.WifiDirectConnectionListener {
@Override
public void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice) {
handler.sendMessage(handler.obtainMessage(CONNECT_TO_SERVICE, serviceDevice.deviceAddress));
public void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice, @NonNull String extraInfo) {
handler.sendMessage(handler.obtainMessage(CONNECT_TO_SERVICE, Integer.parseInt(extraInfo), 0, serviceDevice.deviceAddress));
}
@Override

Wyświetl plik

@ -12,13 +12,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.SelfSignedIdentity.SelfSignedKeys;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
/**
@ -33,46 +30,66 @@ import java.util.concurrent.TimeUnit;
* Testing found that restarting the client worked better than restarting the server when having WiFi
* Direct setup issues.
*/
public final class DeviceTransferServer implements Handler.Callback {
final class DeviceTransferServer implements Handler.Callback {
private static final String TAG = Log.tag(DeviceTransferServer.class);
private static final int START_SERVER = 0;
private static final int START_NETWORK_SERVER = 1;
private static final int NETWORK_DISCONNECTED = 2;
private static final int START_IP_EXCHANGE = 3;
private static final int IP_EXCHANGE_SUCCESS = 4;
private static final String TAG = Log.tag(DeviceTransferServer.class);
private ServerThread serverThread;
private static final int START_SERVER = 0;
private static final int STOP_SERVER = 1;
private static final int START_IP_EXCHANGE = 2;
private static final int IP_EXCHANGE_SUCCESS = 3;
private static final int NETWORK_FAILURE = 4;
private static final int SET_VERIFIED = 5;
private NetworkServerThread serverThread;
private HandlerThread commandAndControlThread;
private final Handler handler;
private WifiDirect wifiDirect;
private final Context context;
private final ServerTask serverTask;
private final int port;
private final ShutdownCallback shutdownCallback;
private IpExchange.IpExchangeThread ipExchangeThread;
private static void update(@NonNull TransferMode transferMode) {
EventBus.getDefault().postSticky(transferMode);
private static void update(@NonNull TransferStatus transferStatus) {
Log.d(TAG, "transferStatus: " + transferStatus.getTransferMode().name());
EventBus.getDefault().postSticky(transferStatus);
}
@AnyThread
public DeviceTransferServer(@NonNull Context context, @NonNull ServerTask serverTask, int port, @Nullable ShutdownCallback shutdownCallback) {
public DeviceTransferServer(@NonNull Context context,
@NonNull ServerTask serverTask,
@Nullable ShutdownCallback shutdownCallback)
{
this.context = context;
this.serverTask = serverTask;
this.port = port;
this.shutdownCallback = shutdownCallback;
this.commandAndControlThread = SignalExecutors.getAndStartHandlerThread("server-cnc");
this.handler = new Handler(commandAndControlThread.getLooper(), this);
}
@AnyThread
public void start() {
handler.sendMessage(handler.obtainMessage(START_SERVER));
public synchronized void start() {
if (commandAndControlThread != null) {
update(TransferStatus.ready());
handler.sendEmptyMessage(START_SERVER);
}
}
@AnyThread
public synchronized void shutdown() {
public synchronized void stop() {
if (commandAndControlThread != null) {
handler.sendEmptyMessage(STOP_SERVER);
}
}
@AnyThread
public synchronized void setVerified(boolean isVerified) {
if (commandAndControlThread != null) {
handler.sendMessage(handler.obtainMessage(SET_VERIFIED, isVerified));
}
}
private synchronized void shutdown() {
stopIpExchange();
stopServer();
stopWifiDirect();
@ -83,19 +100,26 @@ public final class DeviceTransferServer implements Handler.Callback {
commandAndControlThread.interrupt();
commandAndControlThread = null;
}
update(TransferStatus.shutdown());
}
private void internalShutdown() {
shutdown();
if (shutdownCallback != null) {
shutdownCallback.shutdown();
}
}
@Override
public boolean handleMessage(@NonNull Message message) {
Log.d(TAG, "Handle message: " + message.what);
switch (message.what) {
case START_SERVER:
startWifiDirect();
startNetworkServer();
break;
case START_NETWORK_SERVER:
startServer();
break;
case NETWORK_DISCONNECTED:
stopServer();
case STOP_SERVER:
shutdown();
break;
case START_IP_EXCHANGE:
startIpExchange((String) message.obj);
@ -103,188 +127,153 @@ public final class DeviceTransferServer implements Handler.Callback {
case IP_EXCHANGE_SUCCESS:
ipExchangeSuccessful();
break;
default:
shutdown();
if (shutdownCallback != null) {
shutdownCallback.shutdown();
case SET_VERIFIED:
if (serverThread != null) {
serverThread.setVerified((Boolean) message.obj);
}
throw new AssertionError();
break;
case NetworkServerThread.NETWORK_SERVER_STARTED:
startWifiDirect(message.arg1);
break;
case NetworkServerThread.NETWORK_SERVER_STOPPED:
internalShutdown();
break;
case NetworkServerThread.NETWORK_CLIENT_CONNECTED:
stopDiscoveryService();
update(TransferStatus.serviceConnected());
break;
case NetworkServerThread.NETWORK_CLIENT_DISCONNECTED:
update(TransferStatus.networkConnected());
break;
case NetworkServerThread.NETWORK_CLIENT_SSL_ESTABLISHED:
update(TransferStatus.verificationRequired((Integer) message.obj));
break;
default:
internalShutdown();
throw new AssertionError("Unknown message: " + message.what);
}
return false;
}
private void startWifiDirect() {
private void startWifiDirect(int port) {
if (wifiDirect != null) {
Log.e(TAG, "Server already started");
return;
}
update(TransferMode.STARTING_UP);
try {
wifiDirect = new WifiDirect(context);
wifiDirect.initialize(new WifiDirectListener());
wifiDirect.startDiscoveryService();
wifiDirect.startDiscoveryService(String.valueOf(port));
Log.i(TAG, "Started discovery service, waiting for connections...");
update(TransferMode.DISCOVERY);
update(TransferStatus.discovery());
} catch (WifiDirectUnavailableException e) {
Log.e(TAG, e);
shutdown();
update(TransferMode.FAILED);
if (shutdownCallback != null) {
shutdownCallback.shutdown();
internalShutdown();
if (e.getReason() == WifiDirectUnavailableException.Reason.CHANNEL_INITIALIZATION ||
e.getReason() == WifiDirectUnavailableException.Reason.WIFI_P2P_MANAGER) {
update(TransferStatus.unavailable());
} else {
update(TransferStatus.failed());
}
}
}
private void stopDiscoveryService() {
if (wifiDirect == null) {
return;
}
try {
Log.i(TAG, "Stopping discovery service");
wifiDirect.stopDiscoveryService();
} catch (WifiDirectUnavailableException e) {
internalShutdown();
update(TransferStatus.failed());
}
}
private void stopWifiDirect() {
if (wifiDirect != null) {
Log.i(TAG, "Shutting down WiFi Direct");
wifiDirect.shutdown();
wifiDirect = null;
update(TransferMode.READY);
}
}
private void startServer() {
private void startNetworkServer() {
if (serverThread != null) {
Log.i(TAG, "Server already running");
return;
}
Log.i(TAG, "Connection established, spinning up network server.");
serverThread = new ServerThread(context, serverTask, port);
serverThread.start();
update(TransferMode.NETWORK_CONNECTED);
try {
update(TransferStatus.startingUp());
SelfSignedKeys keys = SelfSignedIdentity.create();
Log.i(TAG, "Spinning up network server.");
serverThread = new NetworkServerThread(context, serverTask, keys, handler);
serverThread.start();
} catch (KeyGenerationFailedException e) {
Log.w(TAG, "Error generating keys", e);
internalShutdown();
update(TransferStatus.failed());
}
}
private void stopServer() {
if (serverThread != null) {
Log.i(TAG, "Shutting down ServerThread");
serverThread.shutdown();
try {
serverThread.join(TimeUnit.SECONDS.toMillis(1));
} catch (InterruptedException e) {
Log.i(TAG, "Server thread took too long to shutdown", e);
}
serverThread = null;
}
}
private void startIpExchange(@NonNull String groupOwnerHostAddress) {
ipExchangeThread = IpExchange.giveIp(groupOwnerHostAddress, port, handler, IP_EXCHANGE_SUCCESS);
ipExchangeThread = IpExchange.giveIp(groupOwnerHostAddress, serverThread.getLocalPort(), handler, IP_EXCHANGE_SUCCESS);
}
private void stopIpExchange() {
if (ipExchangeThread != null) {
ipExchangeThread.shutdown();
try {
ipExchangeThread.join(TimeUnit.SECONDS.toMillis(1));
} catch (InterruptedException e) {
Log.i(TAG, "IP Exchange thread took too long to shutdown", e);
}
ipExchangeThread = null;
}
}
private void ipExchangeSuccessful() {
stopIpExchange();
handler.sendEmptyMessage(START_NETWORK_SERVER);
}
public static class ServerThread extends Thread {
private final Context context;
private final ServerTask serverTask;
private final int port;
private volatile ServerSocket serverSocket;
private volatile boolean isRunning;
public ServerThread(@NonNull Context context, @NonNull ServerTask serverTask, int port) {
this.context = context;
this.serverTask = serverTask;
this.port = port;
}
@Override
public void run() {
Log.i(TAG, "Server thread running");
isRunning = true;
while (shouldKeepRunning()) {
Log.i(TAG, "Starting up server socket...");
try {
serverSocket = new ServerSocket(port);
while (shouldKeepRunning() && !serverSocket.isClosed()) {
Log.i(TAG, "Waiting for client socket accept...");
try {
handleClient(serverSocket.accept());
} catch (IOException e) {
if (isRunning) {
Log.i(TAG, "Error connecting with client or server socket closed.", e);
} else {
Log.i(TAG, "Server shutting down...");
}
} finally {
update(TransferMode.NETWORK_CONNECTED);
}
}
} catch (Exception e) {
Log.w(TAG, e);
} finally {
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException ignored) {}
}
update(TransferMode.NETWORK_CONNECTED);
}
if (shouldKeepRunning()) {
ThreadUtil.interruptableSleep(TimeUnit.SECONDS.toMillis(3));
}
}
Log.i(TAG, "Server exiting");
}
public void shutdown() {
isRunning = false;
try {
serverSocket.close();
} catch (IOException e) {
Log.w(TAG, "Error shutting down server socket", e);
}
interrupt();
}
private void handleClient(@NonNull Socket clientSocket) throws IOException {
update(TransferMode.SERVICE_CONNECTED);
serverTask.run(context, clientSocket.getInputStream());
clientSocket.close();
}
private boolean shouldKeepRunning() {
return !isInterrupted() && isRunning;
}
}
public class WifiDirectListener implements WifiDirect.WifiDirectConnectionListener {
@Override
public void onNetworkConnected(@NonNull WifiP2pInfo info) {
if (info.isGroupOwner) {
handler.sendEmptyMessage(START_NETWORK_SERVER);
} else {
if (!info.isGroupOwner) {
handler.sendMessage(handler.obtainMessage(START_IP_EXCHANGE, info.groupOwnerAddress.getHostAddress()));
}
}
@Override
public void onNetworkDisconnected() {
handler.sendEmptyMessage(NETWORK_DISCONNECTED);
}
public void onNetworkDisconnected() { }
@Override
public void onNetworkFailure() {
handler.sendEmptyMessage(NETWORK_DISCONNECTED);
handler.sendEmptyMessage(NETWORK_FAILURE);
}
@Override
public void onLocalDeviceChanged(@NonNull WifiP2pDevice localDevice) { }
@Override
public void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice) { }
public void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice, @NonNull String extraInfo) { }
}
}

Wyświetl plik

@ -3,7 +3,6 @@ package org.signal.devicetransfer;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
@ -22,7 +21,7 @@ import java.util.concurrent.TimeUnit;
* When this occurs, {@link #giveIp(String, int, Handler, int)} and {@link #getIp(String, int, Handler, int)} allow
* the two to connect briefly and use the connected socket to determine the host address of the other.
*/
public final class IpExchange {
final class IpExchange {
private IpExchange() { }
@ -66,7 +65,7 @@ public final class IpExchange {
isRunning = true;
while (shouldKeepRunning()) {
Log.i(TAG, "Attempting to connect to server...");
Log.i(TAG, "Attempting to startup networking...");
try {
if (needsIp) {

Wyświetl plik

@ -0,0 +1,12 @@
package org.signal.devicetransfer;
import androidx.annotation.NonNull;
/**
* Thrown when there's an issue generating the self-signed certificates for TLS.
*/
final class KeyGenerationFailedException extends Throwable {
public KeyGenerationFailedException(@NonNull Exception e) {
super(e);
}
}

Wyświetl plik

@ -0,0 +1,169 @@
package org.signal.devicetransfer;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import static org.signal.devicetransfer.DeviceTransferAuthentication.DIGEST_LENGTH;
/**
* Performs the networking setup/tear down for the client. This includes
* connecting to the server, performing the TLS/SAS verification, running an
* arbitrarily provided {@link ClientTask}, and then cleaning up.
*/
final class NetworkClientThread extends Thread {
private static final String TAG = Log.tag(NetworkClientThread.class);
public static final int NETWORK_CLIENT_CONNECTED = 1001;
public static final int NETWORK_CLIENT_DISCONNECTED = 1002;
public static final int NETWORK_CLIENT_SSL_ESTABLISHED = 1003;
public static final int NETWORK_CLIENT_STOPPED = 1004;
private volatile SSLSocket client;
private volatile boolean isRunning;
private volatile Boolean isVerified;
private final Context context;
private final ClientTask clientTask;
private final String serverHostAddress;
private final int port;
private final Handler handler;
private final Object verificationLock;
private boolean success;
public NetworkClientThread(@NonNull Context context,
@NonNull ClientTask clientTask,
@NonNull String serverHostAddress,
int port,
@NonNull Handler handler)
{
this.context = context;
this.clientTask = clientTask;
this.serverHostAddress = serverHostAddress;
this.port = port;
this.handler = handler;
this.verificationLock = new Object();
}
@Override
public void run() {
Log.i(TAG, "Client thread running");
isRunning = true;
int validClientAttemptsRemaining = 3;
while (shouldKeepRunning()) {
Log.i(TAG, "Attempting to connect to server... tries: " + validClientAttemptsRemaining);
try {
SelfSignedIdentity.ApprovingTrustManager trustManager = new SelfSignedIdentity.ApprovingTrustManager();
client = (SSLSocket) SelfSignedIdentity.getApprovingSocketFactory(trustManager).createSocket();
try {
client.bind(null);
client.connect(new InetSocketAddress(serverHostAddress, port), 10000);
client.startHandshake();
X509Certificate x509 = trustManager.getX509Certificate();
if (x509 == null) {
isRunning = false;
throw new SSLHandshakeException("no x509 after handshake");
}
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
int authenticationCode = DeviceTransferAuthentication.generateClientAuthenticationCode(x509.getEncoded(), inputStream, outputStream);
handler.sendMessage(handler.obtainMessage(NETWORK_CLIENT_SSL_ESTABLISHED, authenticationCode));
Log.i(TAG, "Waiting for user to verify sas");
awaitAuthenticationCodeVerification();
Log.d(TAG, "Waiting for server to tell us they also verified");
//noinspection ResultOfMethodCallIgnored
inputStream.read();
handler.sendEmptyMessage(NETWORK_CLIENT_CONNECTED);
clientTask.run(context, outputStream);
outputStream.flush();
client.shutdownOutput();
Log.d(TAG, "Waiting for server to tell us they got everything");
//noinspection ResultOfMethodCallIgnored
inputStream.read();
success = true;
isRunning = false;
} catch (IOException e) {
Log.w(TAG, "Error connecting to server", e);
validClientAttemptsRemaining--;
isRunning = validClientAttemptsRemaining > 0;
}
} catch (Exception e) {
Log.w(TAG, e);
isRunning = false;
} finally {
StreamUtil.close(client);
handler.sendEmptyMessage(NETWORK_CLIENT_DISCONNECTED);
}
if (shouldKeepRunning()) {
ThreadUtil.interruptableSleep(TimeUnit.SECONDS.toMillis(3));
}
}
Log.i(TAG, "Client exiting");
if (success) {
clientTask.success();
}
handler.sendEmptyMessage(NETWORK_CLIENT_STOPPED);
}
private void awaitAuthenticationCodeVerification() throws DeviceTransferAuthentication.DeviceTransferAuthenticationException {
synchronized (verificationLock) {
try {
while (isVerified == null) {
verificationLock.wait();
}
if (!isVerified) {
throw new DeviceTransferAuthentication.DeviceTransferAuthenticationException("User verification failed");
}
} catch (InterruptedException e) {
throw new DeviceTransferAuthentication.DeviceTransferAuthenticationException(e);
}
}
}
@AnyThread
public void setVerified(boolean isVerified) {
this.isVerified = isVerified;
synchronized (verificationLock) {
verificationLock.notify();
}
}
@AnyThread
public void shutdown() {
isRunning = false;
StreamUtil.close(client);
interrupt();
}
private boolean shouldKeepRunning() {
return !isInterrupted() && isRunning;
}
}

Wyświetl plik

@ -0,0 +1,146 @@
package org.signal.devicetransfer;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
/**
* Performs the networking setup/tear down for the server. This includes
* connecting to the client, generating TLS keys, performing the TLS/SAS verification,
* running an arbitrarily provided {@link ServerTask}, and then cleaning up.
*/
final class NetworkServerThread extends Thread {
private static final String TAG = Log.tag(NetworkServerThread.class);
public static final int NETWORK_SERVER_STARTED = 1001;
public static final int NETWORK_SERVER_STOPPED = 1002;
public static final int NETWORK_CLIENT_CONNECTED = 1003;
public static final int NETWORK_CLIENT_DISCONNECTED = 1004;
public static final int NETWORK_CLIENT_SSL_ESTABLISHED = 1005;
private volatile ServerSocket serverSocket;
private volatile boolean isRunning;
private volatile Boolean isVerified;
private final Context context;
private final ServerTask serverTask;
private final SelfSignedIdentity.SelfSignedKeys keys;
private final Handler handler;
private final Object verificationLock;
public NetworkServerThread(@NonNull Context context,
@NonNull ServerTask serverTask,
@NonNull SelfSignedIdentity.SelfSignedKeys keys,
@NonNull Handler handler)
{
this.context = context;
this.serverTask = serverTask;
this.keys = keys;
this.handler = handler;
this.verificationLock = new Object();
}
@Override
public void run() {
Log.i(TAG, "Server thread running");
isRunning = true;
Log.i(TAG, "Starting up server socket...");
try {
serverSocket = SelfSignedIdentity.getServerSocketFactory(keys).createServerSocket(0);
handler.sendMessage(handler.obtainMessage(NETWORK_SERVER_STARTED, serverSocket.getLocalPort(), 0));
while (shouldKeepRunning() && !serverSocket.isClosed()) {
Log.i(TAG, "Waiting for client socket accept...");
try (Socket clientSocket = serverSocket.accept()) {
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
int authenticationCode = DeviceTransferAuthentication.generateServerAuthenticationCode(keys.getX509Encoded(), inputStream, outputStream);
handler.sendMessage(handler.obtainMessage(NETWORK_CLIENT_SSL_ESTABLISHED, authenticationCode));
Log.i(TAG, "Waiting for user to verify sas");
awaitAuthenticationCodeVerification();
handler.sendEmptyMessage(NETWORK_CLIENT_CONNECTED);
outputStream.write(0x43);
outputStream.flush();
serverTask.run(context, inputStream);
outputStream.write(0x53);
outputStream.flush();
} catch (IOException e) {
if (isRunning) {
Log.i(TAG, "Error connecting with client or server socket closed.", e);
} else {
Log.i(TAG, "Server shutting down...");
}
} finally {
handler.sendEmptyMessage(NETWORK_CLIENT_DISCONNECTED);
}
}
} catch (Exception e) {
Log.w(TAG, e);
} finally {
StreamUtil.close(serverSocket);
}
Log.i(TAG, "Server exiting");
isRunning = false;
handler.sendEmptyMessage(NETWORK_SERVER_STOPPED);
}
private void awaitAuthenticationCodeVerification() throws DeviceTransferAuthentication.DeviceTransferAuthenticationException {
synchronized (verificationLock) {
try {
while (isVerified == null) {
verificationLock.wait();
}
if (!isVerified) {
throw new DeviceTransferAuthentication.DeviceTransferAuthenticationException("User verification failed");
}
} catch (InterruptedException e) {
throw new DeviceTransferAuthentication.DeviceTransferAuthenticationException(e);
}
}
}
private boolean shouldKeepRunning() {
return !isInterrupted() && isRunning;
}
@AnyThread
public int getLocalPort() {
ServerSocket localServerSocket = serverSocket;
if (localServerSocket != null) {
return localServerSocket.getLocalPort();
}
return 0;
}
@AnyThread
public void setVerified(boolean isVerified) {
this.isVerified = isVerified;
synchronized (verificationLock) {
verificationLock.notify();
}
}
@AnyThread
public void shutdown() {
isRunning = false;
StreamUtil.close(serverSocket);
interrupt();
}
}

Wyświetl plik

@ -0,0 +1,170 @@
package org.signal.devicetransfer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.asn1.x500.X500NameBuilder;
import org.spongycastle.asn1.x500.style.BCStyle;
import org.spongycastle.asn1.x509.SubjectPublicKeyInfo;
import org.spongycastle.cert.X509CertificateHolder;
import org.spongycastle.cert.X509v3CertificateBuilder;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.operator.ContentSigner;
import org.spongycastle.operator.OperatorCreationException;
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Generate and configure use of self-signed x509 and private key for establishing a TLS connection.
*/
final class SelfSignedIdentity {
private static final String KEY_GENERATION_ALGORITHM = "RSA";
private static final int KEY_SIZE = 4096;
private static final String SSL_CONTEXT_PROTOCOL = "TLS";
private static final String CERTIFICATE_TYPE = "X509";
private static final String KEYSTORE_TYPE = "BKS";
private static final String SIGNATURE_ALGORITHM = "SHA256WithRSAEncryption";
private SelfSignedIdentity() { }
public static @NonNull SelfSignedKeys create() throws KeyGenerationFailedException {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM);
keyPairGenerator.initialize(KEY_SIZE);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
X509CertificateHolder x509 = createX509(keyPair);
return new SelfSignedKeys(x509.getEncoded(), keyPair.getPrivate());
} catch (GeneralSecurityException | OperatorCreationException | IOException e) {
throw new KeyGenerationFailedException(e);
}
}
public static @NonNull SSLServerSocketFactory getServerSocketFactory(@NonNull SelfSignedKeys keys)
throws GeneralSecurityException, IOException
{
Certificate certificate = CertificateFactory.getInstance(CERTIFICATE_TYPE)
.generateCertificate(new ByteArrayInputStream(keys.getX509Encoded()));
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
keyStore.load(null);
keyStore.setKeyEntry("client", keys.getPrivateKey(), null, new Certificate[]{certificate});
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, null);
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
return sslContext.getServerSocketFactory();
}
public static @NonNull SSLSocketFactory getApprovingSocketFactory(@NonNull ApprovingTrustManager trustManager)
throws GeneralSecurityException
{
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());
return sslContext.getSocketFactory();
}
private static @NonNull X509CertificateHolder createX509(@NonNull KeyPair keyPair) throws OperatorCreationException {
Date startDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
Date endDate = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.C, "United States");
nameBuilder.addRDN(BCStyle.ST, "California");
nameBuilder.addRDN(BCStyle.L, "San Francisco");
nameBuilder.addRDN(BCStyle.O, "Signal Foundation");
nameBuilder.addRDN(BCStyle.CN, "SignalTransfer");
X500Name x500Name = nameBuilder.build();
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextLong()).abs();
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(x500Name,
serialNumber,
startDate,
endDate,
x500Name,
subjectPublicKeyInfo);
Security.addProvider(new BouncyCastleProvider());
ContentSigner signer = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.getPrivate());
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
return certificateBuilder.build(signer);
}
static final class SelfSignedKeys {
private final byte[] x509Encoded;
private final PrivateKey privateKey;
public SelfSignedKeys(@NonNull byte[] x509Encoded, @NonNull PrivateKey privateKey) {
this.x509Encoded = x509Encoded;
this.privateKey = privateKey;
}
public @NonNull byte[] getX509Encoded() {
return x509Encoded;
}
public @NonNull PrivateKey getPrivateKey() {
return privateKey;
}
}
static final class ApprovingTrustManager implements X509TrustManager {
private @Nullable X509Certificate x509Certificate;
@Override
public void checkClientTrusted(@NonNull X509Certificate[] x509Certificates, @NonNull String authType) throws CertificateException {
throw new CertificateException();
}
@Override
public void checkServerTrusted(@NonNull X509Certificate[] x509Certificates, @NonNull String authType) throws CertificateException {
if (x509Certificates.length != 1) {
throw new CertificateException("More than 1 x509 certificate");
}
this.x509Certificate = x509Certificates[0];
}
@Override
public @NonNull X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public @Nullable X509Certificate getX509Certificate() {
return x509Certificate;
}
}
}

Wyświetl plik

@ -5,6 +5,6 @@ package org.signal.devicetransfer;
* {@link DeviceToDeviceTransferService} that an internal issue caused a shutdown and the
* service should stop as well.
*/
public interface ShutdownCallback {
interface ShutdownCallback {
void shutdown();
}

Wyświetl plik

@ -1,12 +0,0 @@
package org.signal.devicetransfer;
public enum TransferMode {
PERMISSIONS,
UNAVAILABLE,
FAILED,
READY,
STARTING_UP,
DISCOVERY,
NETWORK_CONNECTED,
SERVICE_CONNECTED
}

Wyświetl plik

@ -0,0 +1,82 @@
package org.signal.devicetransfer;
import androidx.annotation.NonNull;
/**
* Represents the status of the transfer.
*/
public class TransferStatus {
private final TransferMode transferMode;
private final int authenticationCode;
private TransferStatus(@NonNull TransferMode transferMode) {
this(transferMode, 0);
}
private TransferStatus(int authenticationCode) {
this(TransferMode.VERIFICATION_REQUIRED, authenticationCode);
}
private TransferStatus(@NonNull TransferMode transferMode, int authenticationCode) {
this.transferMode = transferMode;
this.authenticationCode = authenticationCode;
}
public @NonNull TransferMode getTransferMode() {
return transferMode;
}
public int getAuthenticationCode() {
return authenticationCode;
}
public static @NonNull TransferStatus ready() {
return new TransferStatus(TransferMode.READY);
}
public static @NonNull TransferStatus serviceConnected() {
return new TransferStatus(TransferMode.SERVICE_CONNECTED);
}
public static @NonNull TransferStatus networkConnected() {
return new TransferStatus(TransferMode.NETWORK_CONNECTED);
}
public static @NonNull TransferStatus verificationRequired(@NonNull Integer authenticationCode) {
return new TransferStatus(authenticationCode);
}
public static @NonNull TransferStatus startingUp() {
return new TransferStatus(TransferMode.STARTING_UP);
}
public static @NonNull TransferStatus discovery() {
return new TransferStatus(TransferMode.DISCOVERY);
}
public static @NonNull TransferStatus unavailable() {
return new TransferStatus(TransferMode.UNAVAILABLE);
}
public static @NonNull TransferStatus failed() {
return new TransferStatus(TransferMode.FAILED);
}
public static @NonNull TransferStatus shutdown() {
return new TransferStatus(TransferMode.SHUTDOWN);
}
public enum TransferMode {
UNAVAILABLE,
FAILED,
READY,
STARTING_UP,
DISCOVERY,
NETWORK_CONNECTED,
VERIFICATION_REQUIRED,
SERVICE_CONNECTED,
SERVICE_DISCONNECTED,
SHUTDOWN,
}
}

Wyświetl plik

@ -18,9 +18,11 @@ import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceInfo;
import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceRequest;
import android.os.Build;
import android.os.HandlerThread;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
@ -32,12 +34,13 @@ import org.signal.devicetransfer.WifiDirectUnavailableException.Reason;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provide the ability to spin up a WiFi Direct network, advertise a network service,
* discover a network service, and then connect two devices.
*/
@SuppressLint("MissingPermission")
public final class WifiDirect {
private static final String TAG = Log.tag(WifiDirect.class);
@ -49,8 +52,10 @@ public final class WifiDirect {
addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
}};
public static final String SERVICE_INSTANCE = "_devicetransfer._signal.org";
public static final String SERVICE_REG_TYPE = "_presence._tcp";
private static final String EXTRA_INFO_PLACEHOLDER = "%%EXTRA_INFO%%";
private static final String SERVICE_INSTANCE_TEMPLATE = "_devicetransfer" + EXTRA_INFO_PLACEHOLDER + "._signal.org";
private static final Pattern SERVICE_INSTANCE_PATTERN = Pattern.compile("_devicetransfer(\\._(.+))?\\._signal\\.org");
private static final String SERVICE_REG_TYPE = "_presence._tcp";
private final Context context;
private WifiDirectConnectionListener connectionListener;
@ -85,7 +90,7 @@ public final class WifiDirect {
: AvailableStatus.WIFI_DIRECT_NOT_AVAILABLE;
}
public WifiDirect(@NonNull Context context) {
WifiDirect(@NonNull Context context) {
this.context = context.getApplicationContext();
this.wifiDirectCallbacksHandler = SignalExecutors.getAndStartHandlerThread("wifi-direct-cb");
}
@ -95,7 +100,7 @@ public final class WifiDirect {
* with the Android WiFi Direct APIs. This should have a matching call to {@link #shutdown()} to
* release the various resources used to establish and maintain a WiFi Direct network.
*/
public synchronized void initialize(@NonNull WifiDirectConnectionListener connectionListener) throws WifiDirectUnavailableException {
synchronized void initialize(@NonNull WifiDirectConnectionListener connectionListener) throws WifiDirectUnavailableException {
if (isInitialized()) {
Log.w(TAG, "Already initialized, do not need to initialize twice");
return;
@ -128,7 +133,7 @@ public final class WifiDirect {
* <i>Note: After this call, the instance is no longer usable and an entirely new one will need to
* be created.</i>
*/
public synchronized void shutdown() {
synchronized void shutdown() {
Log.d(TAG, "Shutting down");
connectionListener = null;
@ -158,12 +163,15 @@ public final class WifiDirect {
* Start advertising a transfer service that other devices can search for and decide
* to connect to. Call on an appropriate thread as this method synchronously calls WiFi Direct
* methods.
*
* @param extraInfo Extra info to include in the service instance name (e.g., server port)
*/
@WorkerThread
public synchronized void startDiscoveryService() throws WifiDirectUnavailableException {
@SuppressLint("MissingPermission")
synchronized void startDiscoveryService(@NonNull String extraInfo) throws WifiDirectUnavailableException {
ensureInitialized();
WifiP2pDnsSdServiceInfo serviceInfo = WifiP2pDnsSdServiceInfo.newInstance(SERVICE_INSTANCE, SERVICE_REG_TYPE, Collections.emptyMap());
WifiP2pDnsSdServiceInfo serviceInfo = WifiP2pDnsSdServiceInfo.newInstance(buildServiceInstanceName(extraInfo), SERVICE_REG_TYPE, Collections.emptyMap());
SyncActionListener addLocalServiceListener = new SyncActionListener("add local service");
manager.addLocalService(channel, serviceInfo, addLocalServiceListener);
@ -176,12 +184,23 @@ public final class WifiDirect {
}
}
/**
* Stop all peer discovery and advertising services.
*/
synchronized void stopDiscoveryService() throws WifiDirectUnavailableException {
ensureInitialized();
retry(manager::stopPeerDiscovery, "stop peer discovery");
retry(manager::clearLocalServices, "clear local services");
}
/**
* Start searching for a transfer service being advertised by another device. Call on an
* appropriate thread as this method synchronously calls WiFi Direct methods.
*/
@WorkerThread
public synchronized void discoverService() throws WifiDirectUnavailableException {
@SuppressLint("MissingPermission")
synchronized void discoverService() throws WifiDirectUnavailableException {
ensureInitialized();
if (serviceRequest != null) {
@ -192,10 +211,11 @@ public final class WifiDirect {
WifiP2pManager.DnsSdTxtRecordListener txtListener = (fullDomain, record, device) -> {};
WifiP2pManager.DnsSdServiceResponseListener serviceListener = (instanceName, registrationType, sourceDevice) -> {
if (SERVICE_INSTANCE.equals(instanceName)) {
String extraInfo = isInstanceNameMatching(instanceName);
if (extraInfo != null) {
Log.d(TAG, "Service found!");
if (connectionListener != null) {
connectionListener.onServiceDiscovered(sourceDevice);
connectionListener.onServiceDiscovered(sourceDevice, extraInfo);
}
} else {
Log.d(TAG, "Found unusable service, ignoring.");
@ -219,18 +239,29 @@ public final class WifiDirect {
}
}
/**
* Stop searching for transfer services.
*/
synchronized void stopServiceDiscovery() throws WifiDirectUnavailableException {
ensureInitialized();
retry(manager::clearServiceRequests, "clear service requests");
}
/**
* Establish a WiFi Direct network by connecting to the given device address (MAC). An
* address can be found by using {@link #discoverService()}.
*
* @param deviceAddress Device MAC address to establish a connection with
*/
public synchronized void connect(@NonNull String deviceAddress) throws WifiDirectUnavailableException {
@SuppressLint("MissingPermission")
synchronized void connect(@NonNull String deviceAddress) throws WifiDirectUnavailableException {
ensureInitialized();
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = deviceAddress;
config.wps.setup = WpsInfo.PBC;
config.deviceAddress = deviceAddress;
config.wps.setup = WpsInfo.PBC;
config.groupOwnerIntent = 0;
if (serviceRequest != null) {
manager.removeServiceRequest(channel, serviceRequest, LoggingActionListener.message("Remote service request"));
@ -275,6 +306,24 @@ public final class WifiDirect {
}
}
@VisibleForTesting
static @NonNull String buildServiceInstanceName(@Nullable String extraInfo) {
if (TextUtils.isEmpty(extraInfo)) {
return SERVICE_INSTANCE_TEMPLATE.replace(EXTRA_INFO_PLACEHOLDER, "");
}
return SERVICE_INSTANCE_TEMPLATE.replace(EXTRA_INFO_PLACEHOLDER, "._" + extraInfo);
}
@VisibleForTesting
static @Nullable String isInstanceNameMatching(@NonNull String serviceInstanceName) {
Matcher matcher = SERVICE_INSTANCE_PATTERN.matcher(serviceInstanceName);
if (matcher.matches()) {
String extraInfo = matcher.group(2);
return TextUtils.isEmpty(extraInfo) ? "" : extraInfo;
}
return null;
}
private interface ManagerRetry {
void call(@NonNull WifiP2pManager.Channel a, @NonNull WifiP2pManager.ActionListener b);
}
@ -405,7 +454,7 @@ public final class WifiDirect {
public interface WifiDirectConnectionListener {
void onLocalDeviceChanged(@NonNull WifiP2pDevice localDevice);
void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice);
void onServiceDiscovered(@NonNull WifiP2pDevice serviceDevice, @NonNull String extraInfo);
void onNetworkConnected(@NonNull WifiP2pInfo info);

Wyświetl plik

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
/**
* Represents the various type of failure with creating a WiFi Direction connection.
*/
public final class WifiDirectUnavailableException extends Exception {
final class WifiDirectUnavailableException extends Exception {
private final Reason reason;

Wyświetl plik

@ -0,0 +1,88 @@
package org.signal.devicetransfer;
import androidx.annotation.NonNull;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.signal.devicetransfer.DeviceTransferAuthentication.Client;
import org.signal.devicetransfer.DeviceTransferAuthentication.DeviceTransferAuthenticationException;
import org.signal.devicetransfer.DeviceTransferAuthentication.Server;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class DeviceTransferAuthenticationTest {
private static byte[] certificate;
private static byte[] badCertificate;
@BeforeClass
public static void setup() throws KeyGenerationFailedException {
certificate = SelfSignedIdentity.create().getX509Encoded();
badCertificate = SelfSignedIdentity.create().getX509Encoded();
}
@Test
public void testCompute_withNoChanges() throws DeviceTransferAuthenticationException {
Client client = new Client(certificate);
Server server = new Server(certificate, client.getCommitment());
byte[] clientRandom = client.setServerRandomAndGetClientRandom(server.getRandom());
server.setClientRandom(clientRandom);
assertEquals(client.computeShortAuthenticationCode(), server.computeShortAuthenticationCode());
}
@Test(expected = DeviceTransferAuthenticationException.class)
public void testServerCompute_withChangedClientCertificate() throws DeviceTransferAuthenticationException {
Client client = new Client(badCertificate);
Server server = new Server(certificate, client.getCommitment());
byte[] clientRandom = client.setServerRandomAndGetClientRandom(server.getRandom());
server.setClientRandom(clientRandom);
server.computeShortAuthenticationCode();
}
@Test(expected = DeviceTransferAuthenticationException.class)
public void testServerCompute_withChangedClientCommitment() throws DeviceTransferAuthenticationException {
Client client = new Client(certificate);
Server server = new Server(certificate, randomBytes());
byte[] clientRandom = client.setServerRandomAndGetClientRandom(server.getRandom());
server.setClientRandom(clientRandom);
server.computeShortAuthenticationCode();
}
@Test(expected = DeviceTransferAuthenticationException.class)
public void testServerCompute_withChangedClientRandom() throws DeviceTransferAuthenticationException {
Client client = new Client(certificate);
Server server = new Server(certificate, client.getCommitment());
client.setServerRandomAndGetClientRandom(server.getRandom());
server.setClientRandom(randomBytes());
server.computeShortAuthenticationCode();
}
@Test
public void testClientCompute_withChangedServerSecret() throws DeviceTransferAuthenticationException {
Client client = new Client(certificate);
Server server = new Server(certificate, client.getCommitment());
byte[] clientRandom = client.setServerRandomAndGetClientRandom(randomBytes());
server.setClientRandom(clientRandom);
assertNotEquals(client.computeShortAuthenticationCode(), server.computeShortAuthenticationCode());
}
private @NonNull byte[] randomBytes() {
byte[] bytes = new byte[32];
new Random().nextBytes(bytes);
return bytes;
}
}

Wyświetl plik

@ -0,0 +1,42 @@
package org.signal.devicetransfer;
import android.app.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public class WifiDirectTest {
@Test
public void instanceName_withExtraInfo() {
String instanceName = WifiDirect.buildServiceInstanceName("knownothing");
assertEquals("_devicetransfer._knownothing._signal.org", instanceName);
String extractedExtraInfo = WifiDirect.isInstanceNameMatching(instanceName);
assertEquals(extractedExtraInfo, "knownothing");
}
@Test
public void instanceName_matchingWithoutExtraInfo() {
String instanceName = WifiDirect.buildServiceInstanceName("");
assertEquals("_devicetransfer._signal.org", instanceName);
String extractedExtraInfo = WifiDirect.isInstanceNameMatching(instanceName);
assertEquals(extractedExtraInfo, "");
}
@Test
public void instanceName_notMatching() {
String extractedExtraInfo = WifiDirect.isInstanceNameMatching("_whoknows._what.org");
assertNull(extractedExtraInfo);
}
}

Wyświetl plik

@ -6,9 +6,6 @@ dependencyVerification {
['androidx.activity:activity:1.0.0',
'd1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d'],
['androidx.annotation:annotation-experimental:1.0.0',
'b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11'],
['androidx.annotation:annotation:1.1.0',
'd38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692'],
@ -24,15 +21,9 @@ dependencyVerification {
['androidx.arch.core:core-runtime:2.0.0',
'87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e'],
['androidx.cardview:cardview:1.0.0',
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
['androidx.collection:collection:1.1.0',
'632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'],
['androidx.coordinatorlayout:coordinatorlayout:1.1.0',
'44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb'],
['androidx.core:core:1.3.0',
'1c6b6626f15185d8f4bc7caac759412a1ab6e851ecf7526387d9b9fadcabdb63'],
@ -69,15 +60,9 @@ dependencyVerification {
['androidx.loader:loader:1.0.0',
'11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025'],
['androidx.recyclerview:recyclerview:1.1.0',
'f0d2b5a67d0a91ee1b1c73ef2b636a81f3563925ddd15a1d4e1c41ec28de7a4f'],
['androidx.savedstate:savedstate:1.0.0',
'2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83'],
['androidx.transition:transition:1.2.0',
'a1e059b3bc0b43a58dec0efecdcaa89c82d2bca552ea5bacf6656c46e853157e'],
['androidx.vectordrawable:vectordrawable-animated:1.1.0',
'76da2c502371d9c38054df5e2b248d00da87809ed058f3363eae87ce5e2403f8'],
@ -87,19 +72,31 @@ dependencyVerification {
['androidx.versionedparcelable:versionedparcelable:1.1.0',
'9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1'],
['androidx.viewpager2:viewpager2:1.0.0',
'e95c0031d4cc247cd48196c6287e58d2cee54d9c79b85afea7c90920330275af'],
['androidx.viewpager:viewpager:1.0.0',
'147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682'],
['com.google.android.material:material:1.2.1',
'd3d0cc776f2341da8e572586c7d390a5b356ce39a0deb2768071dc40b364ac80'],
['com.google.protobuf:protobuf-javalite:3.10.0',
'215a94dbe100130295906b531bb72a26965c7ac8fcd9a75bf8054a8ac2abf4b4'],
['com.madgag.spongycastle:core:1.58.0.0',
'199617dd5698c5a9312b898c0a4cec7ce9dd8649d07f65d91629f58229d72728'],
['com.madgag.spongycastle:pg:1.54.0.0',
'3f1011ec280c51434dd94396ec25c8d7876d861c0fb1fa9ae70824eddcda2f8f'],
['com.madgag.spongycastle:pkix:1.54.0.0',
'721a302f5ce18bf6fff89d514ef224c37b5dd9ca67a16b56fafaea4b24a51482'],
['com.madgag.spongycastle:prov:1.58.0.0',
'092fd09e7006b0814980513b013d4c2b3ffd24a49a635ab4b2d204bb51af1727'],
['junit:junit:4.12',
'59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a'],
['org.greenrobot:eventbus:3.0.0',
'180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'],
['org.hamcrest:hamcrest-core:1.3',
'66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9'],
]
}