Enable TLS connection and SAS verification between device transfer server and client.
This commit is contained in:
parent
c25250cb05
commit
e74460bd91
23 changed files with 1376 additions and 354 deletions
|
@ -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 {
|
||||
|
|
7
device-transfer/app/proguard/proguard.cfg
Normal file
7
device-transfer/app/proguard/proguard.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
-dontoptimize
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.signal.devicetransfer.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
8
device-transfer/lib/lib-proguard-rules.pro
Normal file
8
device-transfer/lib/lib-proguard-rules.pro
Normal file
|
@ -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** {*;}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package org.signal.devicetransfer;
|
||||
|
||||
public enum TransferMode {
|
||||
PERMISSIONS,
|
||||
UNAVAILABLE,
|
||||
FAILED,
|
||||
READY,
|
||||
STARTING_UP,
|
||||
DISCOVERY,
|
||||
NETWORK_CONNECTED,
|
||||
SERVICE_CONNECTED
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue