Compare commits

...

57 Commits

Author SHA1 Message Date
akwizgran
173b6006c4 Don't treat an incoming connection as an attempt to raise the limit. 2020-05-11 17:15:25 +01:00
akwizgran
99edb893f7 Check for stability whenever connections are closed. 2020-05-11 17:15:25 +01:00
akwizgran
f063feedd4 Simplify backoff. 2020-05-11 17:15:25 +01:00
akwizgran
126f515760 Move responsibility for closing connections from limiter to plugin. 2020-05-11 17:15:25 +01:00
akwizgran
e2b61483d6 Always accept incoming connections. 2020-05-11 17:15:25 +01:00
akwizgran
9771825c45 Back off between attempts to raise connection limit. 2020-05-11 17:15:24 +01:00
akwizgran
e376744487 Update constructor args. 2020-05-11 17:15:24 +01:00
akwizgran
13cca9ca61 Occasionally try to raise the limit by allowing an extra connection. 2020-05-11 17:15:24 +01:00
akwizgran
e464f9e7bd Close connections cleanly when starting key agreement. 2020-05-11 17:15:24 +01:00
akwizgran
bd86ff2d5f Let the limiter know whether connections closed cleanly. 2020-05-11 17:15:24 +01:00
akwizgran
bda3b2100a Raise the connection limit if connections are stable. 2020-05-11 17:15:24 +01:00
akwizgran
104a82aea9 Add unit test for connection limiter. 2020-05-11 17:15:24 +01:00
akwizgran
d905451f48 Impose a fixed limit on the number of Bluetooth connections. 2020-05-11 17:15:24 +01:00
Torsten Grote
708452713d Merge branch '1712-detect-dead-bluetooth-connections' into 'master'
Detect and close dead Bluetooth connections

See merge request briar/briar!1246
2020-05-11 15:55:07 +00:00
akwizgran
c80d3196af Use milliseconds for timing. 2020-05-11 15:42:23 +01:00
Torsten Grote
d1c2eb89a1 Merge branch '1712-fix-double-connection-counting' into 'master'
Don't count Bluetooth connections twice

See merge request briar/briar!1245
2020-05-11 14:06:30 +00:00
akwizgran
c4273d22ed Delegate all other methods to wrapped InputStream. 2020-05-08 16:22:46 +01:00
akwizgran
21f3a9f3c7 Add javadoc. 2020-05-08 16:22:46 +01:00
akwizgran
0281eec0da Add unit test for TimeoutInputStream. 2020-05-08 16:22:46 +01:00
akwizgran
d3fd309609 Only check timeouts when we have some streams to monitor. 2020-05-08 16:22:46 +01:00
akwizgran
f2f278c393 Add timeout monitor for Bluetooth connections. 2020-05-08 16:22:46 +01:00
akwizgran
e204d5a996 Don't count connections twice. 2020-05-08 15:17:27 +01:00
akwizgran
876efee1a8 Use keepalives to detect dead connections. 2020-05-08 14:21:41 +01:00
akwizgran
8fd9a40ffb Merge branch 'discover-bt-address-from-incoming-connection' into 'master'
Discover remote Bluetooth address from connection

See merge request briar/briar!1244
2020-04-29 15:31:30 +00:00
akwizgran
fb918457d4 Use constants for metadata keys. 2020-04-29 15:37:21 +01:00
akwizgran
b5fe55faf3 Validate remote address. 2020-04-29 15:28:27 +01:00
akwizgran
7320099494 Also store properties discovered from outgoing connections.
This is useful when adding a Bluetooth address is discovered while
adding a contact.
2020-04-28 17:56:01 +01:00
akwizgran
346bec94e8 Discover contacts' BT addresses from incoming connections. 2020-04-28 17:45:17 +01:00
akwizgran
856ec61759 Merge branch '1722-lastChatActivity' into 'master'
Include last private chat activity in list of contacts

Closes #1722

See merge request briar/briar!1242
2020-04-27 09:20:08 +00:00
Torsten Grote
f61e2b399e [headless] Fix unit tests by passing only timestamp into OutputContact 2020-04-20 09:44:51 -03:00
Nico Alt
6135f9152f Include last private chat activity in list of contacts
Fixes #1722.
2020-04-08 12:00:00 +00:00
Torsten Grote
84584d4d3c Merge branch 'tor-0.3.5.10' into 'master'
Upgrade Tor to version 0.3.5.10

Closes #1714

See merge request briar/briar!1241
2020-03-31 13:14:21 +00:00
akwizgran
17239810c8 Upgrade Tor to version 0.3.5.10. 2020-03-25 17:09:12 +00:00
Torsten Grote
9eee58657e Merge branch '1696-keystore-crash' into 'master'
Show a dialog instead of crashing if a hardware-backed key can't be loaded

Closes #1696

See merge request briar/briar!1233
2020-03-12 12:12:42 +00:00
Torsten Grote
76425455b8 Merge branch 'logging-for-account-bugs' into 'master'
Add logging to track down account bugs

See merge request briar/briar!1239
2020-03-11 14:18:50 +00:00
akwizgran
9ea7140a7f Add logging to track down account bugs. 2020-03-11 14:06:48 +00:00
akwizgran
bde9800c89 Add annotation for visibility. 2020-03-11 13:54:01 +00:00
Torsten Grote
4e5b6ed3e0 Merge branch '1367-db-race' into 'master'
Don't infer anything from existence of (possibly empty) DB directory

Closes #1528 and #1367

See merge request briar/briar!1238
2020-03-10 14:59:06 +00:00
akwizgran
77d037f061 Update javadocs. 2020-03-10 11:27:54 +00:00
Torsten Grote
676f5faef4 Merge branch 'fix-wifi-connectivity-misreporting' into 'master'
Fix misreporting of wifi status in LAN plugin

See merge request briar/briar!1237
2020-03-06 16:52:20 +00:00
akwizgran
8e21068465 Fix misreporting of wifi status in LAN plugin. 2020-03-06 13:35:06 +00:00
akwizgran
4a68e5347d Merge branch '1582-fix-climbing-snackbar' into 'master'
Fix climbing snackbar

Closes #1582

See merge request briar/briar!1223
2020-03-03 14:42:20 +00:00
Torsten Grote
27dd383496 Merge branch '1371-protect-code-cache-directory' into 'master'
Protect cache and code_cache directories when deleting account

Closes #1545 and #1371

See merge request briar/briar!1231
2020-02-26 14:03:39 +00:00
akwizgran
ed50582e27 Show a dialog if the DB key can't be decrypted due to a keystore error. 2020-02-25 15:00:49 +00:00
akwizgran
1546a05568 Catch exception if hardware-backed key can't be loaded. 2020-02-25 12:28:21 +00:00
akwizgran
4bdf966e67 Test that code_cache directory isn't deleted. 2020-02-25 11:23:07 +00:00
akwizgran
e1e67f3b2e Clear the cache directory but don't delete it. 2020-02-25 11:18:50 +00:00
akwizgran
1d63b16ff1 Don't delete the code_cache directory when deleting account.
This seems to avoid the disappearing account bug when installing a new
version.
2020-02-25 10:14:31 +00:00
akwizgran
618ab1f1ec Don't infer anything from existence of (possibly empty) DB directory. 2020-02-24 17:51:59 +00:00
Torsten Grote
421f0ebfa5 Merge branch 'network-prefix-length' into 'master'
Use network prefix length to determine which addresses are connectable

Closes #1178

See merge request briar/briar!1230
2020-02-19 13:11:24 +00:00
akwizgran
61db5d1b04 Make bit-twiddling code more readable. 2020-02-19 09:52:13 +00:00
akwizgran
b3d4012527 Use network prefix length to determine which addresses are connectable. 2020-02-18 11:22:29 +00:00
Torsten Grote
60172331ee Merge branch 'ipv4-link-local' into 'master'
Add support for IPv4 link-local addresses

See merge request briar/briar!1229
2020-02-17 12:42:01 +00:00
akwizgran
076debdc4b Merge branch '1328-reuse-port' into 'master'
Choose port in advance when providing wifi access point

Closes #1328

See merge request briar/briar!1228
2020-02-17 12:37:29 +00:00
akwizgran
ed13cbca6a Add support for IPv4 link-local addresses. 2020-02-17 11:42:13 +00:00
akwizgran
49cb1d0612 Choose port in advance when providing wifi access point. 2020-02-14 16:56:00 +00:00
Torsten Grote
1e4ad67ffc [android] Fix climbing snackbar
Use a fresh snackbar for pending contacts each time it needs to be
shown. Don't re-use the old instance and clear it in onStop().
2020-02-11 13:25:15 -03:00
87 changed files with 2075 additions and 871 deletions

View File

@@ -38,7 +38,7 @@ configurations {
dependencies { dependencies {
implementation project(path: ':bramble-core', configuration: 'default') implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.9@zip' tor 'org.briarproject:tor-android:0.3.5.10@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip' tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24' annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import java.io.File; import java.io.File;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -20,6 +21,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject; import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir; import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir; import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
@@ -30,6 +32,12 @@ class AndroidAccountManager extends AccountManagerImpl
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(AndroidAccountManager.class.getName()); Logger.getLogger(AndroidAccountManager.class.getName());
/**
* Directories that shouldn't be deleted when deleting the user's account.
*/
private static final List<String> PROTECTED_DIR_NAMES =
asList("cache", "code_cache", "lib", "shared_prefs");
protected final Context appContext; protected final Context appContext;
private final SharedPreferences prefs; private final SharedPreferences prefs;
@@ -81,7 +89,7 @@ class AndroidAccountManager extends AccountManagerImpl
if (!prefs.edit().clear().commit()) if (!prefs.edit().clear().commit())
LOG.warning("Could not clear shared preferences"); LOG.warning("Could not clear shared preferences");
} }
// Delete files, except lib and shared_prefs directories // Delete files, except protected directories
Set<File> files = new HashSet<>(); Set<File> files = new HashSet<>();
File dataDir = getDataDir(); File dataDir = getDataDir();
@Nullable @Nullable
@@ -90,14 +98,12 @@ class AndroidAccountManager extends AccountManagerImpl
LOG.warning("Could not list files in app data dir"); LOG.warning("Could not list files in app data dir");
} else { } else {
for (File file : fileArray) { for (File file : fileArray) {
String name = file.getName(); if (!PROTECTED_DIR_NAMES.contains(file.getName())) {
if (!name.equals("lib") && !name.equals("shared_prefs")) {
files.add(file); files.add(file);
} }
} }
} }
files.add(appContext.getFilesDir()); files.add(appContext.getFilesDir());
files.add(appContext.getCacheDir());
addIfNotNull(files, appContext.getExternalCacheDir()); addIfNotNull(files, appContext.getExternalCacheDir());
if (SDK_INT >= 19) { if (SDK_INT >= 19) {
for (File file : appContext.getExternalCacheDirs()) { for (File file : appContext.getExternalCacheDirs()) {
@@ -109,12 +115,16 @@ class AndroidAccountManager extends AccountManagerImpl
addIfNotNull(files, file); addIfNotNull(files, file);
} }
} }
// Clear the cache directory but don't delete it
File cacheDir = appContext.getCacheDir();
File[] children = cacheDir.listFiles();
if (children != null) files.addAll(asList(children));
for (File file : files) { for (File file : files) {
if (LOG.isLoggable(INFO)) {
LOG.info("Deleting " + file.getAbsolutePath());
}
deleteFileOrDir(file); deleteFileOrDir(file);
} }
// Recreate the cache dir as some OpenGL drivers expect it to exist
if (!new File(dataDir, "cache").mkdirs())
LOG.warning("Could not recreate cache dir");
} }
private File getDataDir() { private File getDataDir() {

View File

@@ -32,6 +32,7 @@ import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON; import static android.content.Intent.ACTION_SCREEN_ON;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED; import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
@@ -76,9 +77,9 @@ class AndroidNetworkManager implements NetworkManager, Service {
filter.addAction(ACTION_SCREEN_ON); filter.addAction(ACTION_SCREEN_ON);
filter.addAction(ACTION_SCREEN_OFF); filter.addAction(ACTION_SCREEN_OFF);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION); filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED); if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
appContext.registerReceiver(networkStateReceiver, filter); appContext.registerReceiver(networkStateReceiver, filter);
} }
@Override @Override
@@ -136,7 +137,8 @@ class AndroidNetworkManager implements NetworkManager, Service {
} }
private boolean isApEvent(@Nullable String action) { private boolean isApEvent(@Nullable String action) {
return WIFI_AP_STATE_CHANGED_ACTION.equals(action); return WIFI_AP_STATE_CHANGED_ACTION.equals(action) ||
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action);
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -76,11 +77,12 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
private volatile BluetoothAdapter adapter = null; private volatile BluetoothAdapter adapter = null;
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
Executor ioExecutor, AndroidExecutor androidExecutor, TimeoutMonitor timeoutMonitor, Executor ioExecutor,
Context appContext, SecureRandom secureRandom, Clock clock, SecureRandom secureRandom, AndroidExecutor androidExecutor,
Backoff backoff, PluginCallback callback, int maxLatency) { Context appContext, Clock clock, Backoff backoff,
super(connectionLimiter, ioExecutor, secureRandom, backoff, callback, PluginCallback callback, int maxLatency, int maxIdleTime) {
maxLatency); super(connectionLimiter, timeoutMonitor, ioExecutor, secureRandom,
backoff, callback, maxLatency, maxIdleTime);
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.clock = clock; this.clock = clock;
@@ -172,9 +174,10 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
return wrapSocket(ss.accept()); return wrapSocket(ss.accept());
} }
private DuplexTransportConnection wrapSocket(BluetoothSocket s) { private DuplexTransportConnection wrapSocket(BluetoothSocket s)
return new AndroidBluetoothTransportConnection(this, throws IOException {
connectionLimiter, s); return new AndroidBluetoothTransportConnection(this, connectionLimiter,
timeoutMonitor, s);
} }
@Override @Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.bluetooth;
import android.content.Context; import android.content.Context;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.BackoffFactory; import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -25,6 +26,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
public class AndroidBluetoothPluginFactory implements DuplexPluginFactory { public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
@@ -35,18 +37,20 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final EventBus eventBus; private final EventBus eventBus;
private final Clock clock; private final Clock clock;
private final TimeoutMonitor timeoutMonitor;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
public AndroidBluetoothPluginFactory(Executor ioExecutor, public AndroidBluetoothPluginFactory(Executor ioExecutor,
AndroidExecutor androidExecutor, Context appContext, AndroidExecutor androidExecutor, Context appContext,
SecureRandom secureRandom, EventBus eventBus, Clock clock, SecureRandom secureRandom, EventBus eventBus, Clock clock,
BackoffFactory backoffFactory) { TimeoutMonitor timeoutMonitor, BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.eventBus = eventBus; this.eventBus = eventBus;
this.clock = clock; this.clock = clock;
this.timeoutMonitor = timeoutMonitor;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
} }
@@ -63,12 +67,13 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
@Override @Override
public DuplexPlugin createPlugin(PluginCallback callback) { public DuplexPlugin createPlugin(PluginCallback callback) {
BluetoothConnectionLimiter connectionLimiter = BluetoothConnectionLimiter connectionLimiter =
new BluetoothConnectionLimiterImpl(); new BluetoothConnectionLimiterImpl(eventBus, clock);
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin( AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin(
connectionLimiter, ioExecutor, androidExecutor, appContext, connectionLimiter, timeoutMonitor, ioExecutor, secureRandom,
secureRandom, clock, backoff, callback, MAX_LATENCY); androidExecutor, appContext, clock, backoff,
callback, MAX_LATENCY, MAX_IDLE_TIME);
eventBus.addListener(plugin); eventBus.addListener(plugin);
return plugin; return plugin;
} }

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin.bluetooth;
import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothSocket;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin; import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
@@ -10,24 +11,33 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.util.AndroidUtils.isValidBluetoothAddress;
@NotNullByDefault @NotNullByDefault
class AndroidBluetoothTransportConnection class AndroidBluetoothTransportConnection
extends AbstractDuplexTransportConnection { extends AbstractDuplexTransportConnection {
private final BluetoothConnectionLimiter connectionManager; private final BluetoothConnectionLimiter connectionLimiter;
private final BluetoothSocket socket; private final BluetoothSocket socket;
private final InputStream in;
AndroidBluetoothTransportConnection(Plugin plugin, AndroidBluetoothTransportConnection(Plugin plugin,
BluetoothConnectionLimiter connectionManager, BluetoothConnectionLimiter connectionLimiter,
BluetoothSocket socket) { TimeoutMonitor timeoutMonitor, BluetoothSocket socket)
throws IOException {
super(plugin); super(plugin);
this.connectionManager = connectionManager; this.connectionLimiter = connectionLimiter;
this.socket = socket; this.socket = socket;
in = timeoutMonitor.createTimeoutInputStream(
socket.getInputStream(), plugin.getMaxIdleTime() * 2);
String address = socket.getRemoteDevice().getAddress();
if (isValidBluetoothAddress(address)) remote.put(PROP_ADDRESS, address);
} }
@Override @Override
protected InputStream getInputStream() throws IOException { protected InputStream getInputStream() {
return socket.getInputStream(); return in;
} }
@Override @Override
@@ -40,7 +50,7 @@ class AndroidBluetoothTransportConnection
try { try {
socket.close(); socket.close();
} finally { } finally {
connectionManager.connectionClosed(this); connectionLimiter.connectionClosed(this, exception);
} }
} }
} }

View File

@@ -19,7 +19,7 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Collection; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -40,19 +40,6 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
private static final Logger LOG = private static final Logger LOG =
getLogger(AndroidLanTcpPlugin.class.getName()); getLogger(AndroidLanTcpPlugin.class.getName());
private static final byte[] WIFI_AP_ADDRESS_BYTES =
{(byte) 192, (byte) 168, 43, 1};
private static final InetAddress WIFI_AP_ADDRESS;
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(WIFI_AP_ADDRESS_BYTES);
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
private final Executor connectionStatusExecutor; private final Executor connectionStatusExecutor;
private final ConnectivityManager connectivityManager; private final ConnectivityManager connectivityManager;
@Nullable @Nullable
@@ -62,8 +49,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
AndroidLanTcpPlugin(Executor ioExecutor, Context appContext, AndroidLanTcpPlugin(Executor ioExecutor, Context appContext,
Backoff backoff, PluginCallback callback, int maxLatency, Backoff backoff, PluginCallback callback, int maxLatency,
int maxIdleTime) { int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime); super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
// Don't execute more than one connection status check at a time // Don't execute more than one connection status check at a time
connectionStatusExecutor = connectionStatusExecutor =
new PoliteExecutor("AndroidLanTcpPlugin", ioExecutor, 1); new PoliteExecutor("AndroidLanTcpPlugin", ioExecutor, 1);
@@ -79,6 +67,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
@Override @Override
public void start() { public void start() {
if (used.getAndSet(true)) throw new IllegalStateException(); if (used.getAndSet(true)) throw new IllegalStateException();
initialisePortProperty();
running = true; running = true;
updateConnectionStatus(); updateConnectionStatus();
} }
@@ -95,16 +84,19 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
} }
@Override @Override
protected Collection<InetAddress> getLocalIpAddresses() { protected List<InetAddress> getUsableLocalInetAddresses() {
// If the device doesn't have wifi, don't open any sockets // If the device doesn't have wifi, don't open any sockets
if (wifiManager == null) return emptyList(); if (wifiManager == null) return emptyList();
// If we're connected to a wifi network, use that network // If we're connected to a wifi network, return its address
WifiInfo info = wifiManager.getConnectionInfo(); WifiInfo info = wifiManager.getConnectionInfo();
if (info != null && info.getIpAddress() != 0) if (info != null && info.getIpAddress() != 0) {
return singletonList(intToInetAddress(info.getIpAddress())); return singletonList(intToInetAddress(info.getIpAddress()));
}
// If we're running an access point, return its address // If we're running an access point, return its address
if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS)) for (InetAddress addr : getLocalInetAddresses()) {
return singletonList(WIFI_AP_ADDRESS); if (addr.equals(WIFI_AP_ADDRESS)) return singletonList(addr);
if (addr.equals(WIFI_DIRECT_AP_ADDRESS)) return singletonList(addr);
}
// No suitable addresses // No suitable addresses
return emptyList(); return emptyList();
} }
@@ -144,8 +136,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
private void updateConnectionStatus() { private void updateConnectionStatus() {
connectionStatusExecutor.execute(() -> { connectionStatusExecutor.execute(() -> {
if (!running) return; if (!running) return;
Collection<InetAddress> addrs = getLocalIpAddresses(); List<InetAddress> addrs = getUsableLocalInetAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)) { if (addrs.contains(WIFI_AP_ADDRESS)
|| addrs.contains(WIFI_DIRECT_AP_ADDRESS)) {
LOG.info("Providing wifi hotspot"); LOG.info("Providing wifi hotspot");
// There's no corresponding Network object and thus no way // There's no corresponding Network object and thus no way
// to get a suitable socket factory, so we won't be able to // to get a suitable socket factory, so we won't be able to

View File

@@ -21,10 +21,11 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault @NotNullByDefault
public class AndroidLanTcpPluginFactory implements DuplexPluginFactory { public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
@@ -55,7 +56,8 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
AndroidLanTcpPlugin plugin = new AndroidLanTcpPlugin(ioExecutor, AndroidLanTcpPlugin plugin = new AndroidLanTcpPlugin(ioExecutor,
appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME); appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME,
CONNECTION_TIMEOUT);
eventBus.addListener(plugin); eventBus.addListener(plugin);
return plugin; return plugin;
} }

View File

@@ -71,7 +71,7 @@ public class AndroidUtils {
return new Pair<>("", ""); return new Pair<>("", "");
} }
private static boolean isValidBluetoothAddress(@Nullable String address) { public static boolean isValidBluetoothAddress(@Nullable String address) {
return !StringUtils.isNullOrEmpty(address) return !StringUtils.isNullOrEmpty(address)
&& BluetoothAdapter.checkBluetoothAddress(address) && BluetoothAdapter.checkBluetoothAddress(address)
&& !address.equals(FAKE_BLUETOOTH_ADDRESS); && !address.equals(FAKE_BLUETOOTH_ADDRESS);

View File

@@ -72,7 +72,9 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
@Test @Test
public void testDeleteAccountClearsSharedPrefsAndDeletesFiles() public void testDeleteAccountClearsSharedPrefsAndDeletesFiles()
throws Exception { throws Exception {
// Directories 'lib' and 'shared_prefs' should be spared // Directories 'code_cache', 'lib' and 'shared_prefs' should be spared
File codeCacheDir = new File(testDir, "code_cache");
File codeCacheFile = new File(codeCacheDir, "file");
File libDir = new File(testDir, "lib"); File libDir = new File(testDir, "lib");
File libFile = new File(libDir, "file"); File libFile = new File(libDir, "file");
File sharedPrefsDir = new File(testDir, "shared_prefs"); File sharedPrefsDir = new File(testDir, "shared_prefs");
@@ -111,6 +113,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertTrue(dbDir.mkdirs()); assertTrue(dbDir.mkdirs());
assertTrue(keyDir.mkdirs()); assertTrue(keyDir.mkdirs());
assertTrue(codeCacheDir.mkdirs());
assertTrue(codeCacheFile.createNewFile());
assertTrue(libDir.mkdirs()); assertTrue(libDir.mkdirs());
assertTrue(libFile.createNewFile()); assertTrue(libFile.createNewFile());
assertTrue(sharedPrefsDir.mkdirs()); assertTrue(sharedPrefsDir.mkdirs());
@@ -126,6 +130,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertFalse(dbDir.exists()); assertFalse(dbDir.exists());
assertFalse(keyDir.exists()); assertFalse(keyDir.exists());
assertTrue(codeCacheDir.exists());
assertTrue(codeCacheFile.exists());
assertTrue(libDir.exists()); assertTrue(libDir.exists());
assertTrue(libFile.exists()); assertTrue(libFile.exists());
assertTrue(sharedPrefsDir.exists()); assertTrue(sharedPrefsDir.exists());

View File

@@ -70,7 +70,7 @@ dependencyVerification {
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca', 'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349', 'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a', 'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.9:tor-android-0.3.5.9.zip:853b0440feccd6904bd03e6b2de53a62ebcde1d58068beeadc447a7dff950bc8', 'org.briarproject:tor-android:0.3.5.10:tor-android-0.3.5.10.zip:edd83bf557fcff2105eaa0bdb3f607a6852ebe7360920929ae3039dd5f4774c5',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d', 'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a', 'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0', 'org.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0',

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.api.account; package org.briarproject.bramble.api.account;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -13,7 +14,8 @@ public interface AccountManager {
* Returns true if the manager has the database key. This will be false * Returns true if the manager has the database key. This will be false
* before {@link #createAccount(String, String)} or {@link #signIn(String)} * before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and true after {@link #createAccount(String, String)} * has been called, and true after {@link #createAccount(String, String)}
* or {@link #signIn(String)} has returned true, until the process exits. * or {@link #signIn(String)} has returned true, until
* {@link #deleteAccount()} is called or the process exits.
*/ */
boolean hasDatabaseKey(); boolean hasDatabaseKey();
@@ -22,25 +24,22 @@ public interface AccountManager {
* before {@link #createAccount(String, String)} or {@link #signIn(String)} * before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and non-null after * has been called, and non-null after
* {@link #createAccount(String, String)} or {@link #signIn(String)} has * {@link #createAccount(String, String)} or {@link #signIn(String)} has
* returned true, until the process exits. * returned true, until {@link #deleteAccount()} is called or the process
* exits.
*/ */
@Nullable @Nullable
SecretKey getDatabaseKey(); SecretKey getDatabaseKey();
/** /**
* Returns true if the encrypted database key can be loaded from disk, and * Returns true if the encrypted database key can be loaded from disk.
* the database directory exists and is a directory.
*/ */
boolean accountExists(); boolean accountExists();
/** /**
* Creates an identity with the given name and registers it with the * Creates an identity with the given name and registers it with the
* {@link IdentityManager}. Creates a database key, encrypts it with the * {@link IdentityManager}. Creates a database key, encrypts it with the
* given password and stores it on disk. * given password and stores it on disk. {@link #accountExists()} will
* <p/> * return true after this method returns true.
* This method does not create the database directory, so
* {@link #accountExists()} will continue to return false until the
* database directory is created.
*/ */
boolean createAccount(String name, String password); boolean createAccount(String name, String password);
@@ -54,17 +53,19 @@ public interface AccountManager {
* Loads the encrypted database key from disk and decrypts it with the * Loads the encrypted database key from disk and decrypts it with the
* given password. * given password.
* *
* @return true if the database key was successfully loaded and decrypted. * @throws DecryptionException If the database key could not be loaded and
* decrypted.
*/ */
boolean signIn(String password); void signIn(String password) throws DecryptionException;
/** /**
* Loads the encrypted database key from disk, decrypts it with the old * Loads the encrypted database key from disk, decrypts it with the old
* password, encrypts it with the new password, and stores it on disk, * password, encrypts it with the new password, and stores it on disk,
* replacing the old key. * replacing the old key.
* *
* @return true if the database key was successfully loaded, re-encrypted * @throws DecryptionException If the database key could not be loaded and
* and stored. * decrypted.
*/ */
boolean changePassword(String oldPassword, String newPassword); void changePassword(String oldPassword, String newPassword)
throws DecryptionException;
} }

View File

@@ -142,16 +142,17 @@ public interface CryptoComponent {
/** /**
* Decrypts and authenticates the given ciphertext that has been read from * Decrypts and authenticates the given ciphertext that has been read from
* storage. The encryption and authentication keys are derived from the * storage. The encryption and authentication keys are derived from the
* given password. Returns null if the ciphertext cannot be decrypted and * given password.
* authenticated (for example, if the password is wrong).
* *
* @param keyStrengthener Used to strengthen the password-based key. If * @param keyStrengthener Used to strengthen the password-based key. If
* null, or if strengthening was not used when encrypting the ciphertext, * null, or if strengthening was not used when encrypting the ciphertext,
* the password-based key will not be strengthened * the password-based key will not be strengthened
* @throws DecryptionException If the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
*/ */
@Nullable
byte[] decryptWithPassword(byte[] ciphertext, String password, byte[] decryptWithPassword(byte[] ciphertext, String password,
@Nullable KeyStrengthener keyStrengthener); @Nullable KeyStrengthener keyStrengthener)
throws DecryptionException;
/** /**
* Returns true if the given ciphertext was encrypted using a strengthened * Returns true if the given ciphertext was encrypted using a strengthened

View File

@@ -0,0 +1,17 @@
package org.briarproject.bramble.api.crypto;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class DecryptionException extends Exception {
private final DecryptionResult result;
public DecryptionException(DecryptionResult result) {
this.result = result;
}
public DecryptionResult getDecryptionResult() {
return result;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.api.crypto;
/**
* The result of a password-based decryption operation.
*/
public enum DecryptionResult {
/**
* Decryption succeeded.
*/
SUCCESS,
/**
* Decryption failed because the format of the ciphertext was invalid.
*/
INVALID_CIPHERTEXT,
/**
* Decryption failed because the {@link KeyStrengthener} used for
* encryption was not available for decryption.
*/
KEY_STRENGTHENER_ERROR,
/**
* Decryption failed because the password used for decryption did not match
* the password used for encryption.
*/
INVALID_PASSWORD
}

View File

@@ -0,0 +1,15 @@
package org.briarproject.bramble.api.io;
import java.io.InputStream;
public interface TimeoutMonitor {
/**
* Returns an {@link InputStream} that wraps the given stream and allows
* read timeouts to be detected.
*
* @param timeoutMs The read timeout in milliseconds. Timeouts will be
* detected eventually but are not guaranteed to be detected immediately.
*/
InputStream createTimeoutInputStream(InputStream in, long timeoutMs);
}

View File

@@ -4,10 +4,10 @@ public interface LanTcpConstants {
TransportId ID = new TransportId("org.briarproject.bramble.lan"); TransportId ID = new TransportId("org.briarproject.bramble.lan");
// a transport property (shared with contacts) // Transport properties (shared with contacts)
String PROP_IP_PORTS = "ipPorts"; String PROP_IP_PORTS = "ipPorts";
String PROP_PORT = "port";
// a local setting // A local setting
String PREF_LAN_IP_PORTS = "ipPorts"; String PREF_LAN_IP_PORTS = "ipPorts";
} }

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin; import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter; import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -14,6 +15,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractDuplexTransportConnection public abstract class AbstractDuplexTransportConnection
implements DuplexTransportConnection { implements DuplexTransportConnection {
protected final TransportProperties remote = new TransportProperties();
private final Plugin plugin; private final Plugin plugin;
private final Reader reader; private final Reader reader;
private final Writer writer; private final Writer writer;
@@ -44,6 +47,11 @@ public abstract class AbstractDuplexTransportConnection
return writer; return writer;
} }
@Override
public TransportProperties getRemoteProperties() {
return remote;
}
private class Reader implements TransportConnectionReader { private class Reader implements TransportConnectionReader {
@Override @Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter; import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
/** /**
* An interface for reading and writing data over a duplex transport. The * An interface for reading and writing data over a duplex transport. The
@@ -23,4 +24,10 @@ public interface DuplexTransportConnection {
* for writing to the connection. * for writing to the connection.
*/ */
TransportConnectionWriter getWriter(); TransportConnectionWriter getWriter();
/**
* Returns a possibly empty set of {@link TransportProperties} describing
* the remote peer.
*/
TransportProperties getRemoteProperties();
} }

View File

@@ -11,4 +11,28 @@ public interface TransportPropertyConstants {
* The maximum length of a property's key or value in UTF-8 bytes. * The maximum length of a property's key or value in UTF-8 bytes.
*/ */
int MAX_PROPERTY_LENGTH = 100; int MAX_PROPERTY_LENGTH = 100;
/**
* Message metadata key for the transport ID of a local or remote update,
* as a BDF string.
*/
String MSG_KEY_TRANSPORT_ID = "transportId";
/**
* Message metadata key for the version number of a local or remote update,
* as a BDF long.
*/
String MSG_KEY_VERSION = "version";
/**
* Message metadata key for whether an update is local or remote, as a BDF
* boolean.
*/
String MSG_KEY_LOCAL = "local";
/**
* Group metadata key for any discovered transport properties of the
* contact, as a BDF dictionary.
*/
String GROUP_KEY_DISCOVERED = "discovered";
} }

View File

@@ -34,6 +34,14 @@ public interface TransportPropertyManager {
void addRemoteProperties(Transaction txn, ContactId c, void addRemoteProperties(Transaction txn, ContactId c,
Map<TransportId, TransportProperties> props) throws DbException; Map<TransportId, TransportProperties> props) throws DbException;
/**
* Stores the given properties discovered from an incoming transport
* connection. They will be overridden by any properties received while
* adding the contact or synced from the contact.
*/
void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException;
/** /**
* Returns the local transport properties for all transports. * Returns the local transport properties for all transports.
*/ */

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.transport.StreamWriter; import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.InputStream; import java.io.InputStream;
@@ -11,9 +12,9 @@ public interface SyncSessionFactory {
SyncSession createIncomingSession(ContactId c, InputStream in); SyncSession createIncomingSession(ContactId c, InputStream in);
SyncSession createSimplexOutgoingSession(ContactId c, int maxLatency, SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
StreamWriter streamWriter); int maxLatency, StreamWriter streamWriter);
SyncSession createDuplexOutgoingSession(ContactId c, int maxLatency, SyncSession createDuplexOutgoingSession(ContactId c, TransportId t,
int maxIdleTime, StreamWriter streamWriter); int maxLatency, int maxIdleTime, StreamWriter streamWriter);
} }

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.api.sync.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when all sync connections using a given
* transport should be closed.
*/
@Immutable
@NotNullByDefault
public class CloseSyncConnectionsEvent extends Event {
private final TransportId transportId;
public CloseSyncConnectionsEvent(TransportId transportId) {
this.transportId = transportId;
}
public TransportId getTransportId() {
return transportId;
}
}

View File

@@ -117,4 +117,10 @@ public class IoUtils {
throw new IOException(e); throw new IOException(e);
} }
} }
public static boolean isNonEmptyDirectory(File f) {
if (!f.isDirectory()) return false;
File[] children = f.listFiles();
return children != null && children.length > 0;
}
} }

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.db.DatabaseExecutorModule;
import org.briarproject.bramble.db.DatabaseModule; import org.briarproject.bramble.db.DatabaseModule;
import org.briarproject.bramble.event.EventModule; import org.briarproject.bramble.event.EventModule;
import org.briarproject.bramble.identity.IdentityModule; import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.io.IoModule;
import org.briarproject.bramble.keyagreement.KeyAgreementModule; import org.briarproject.bramble.keyagreement.KeyAgreementModule;
import org.briarproject.bramble.lifecycle.LifecycleModule; import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.plugin.PluginModule; import org.briarproject.bramble.plugin.PluginModule;
@@ -35,6 +36,7 @@ import dagger.Module;
DatabaseExecutorModule.class, DatabaseExecutorModule.class,
EventModule.class, EventModule.class,
IdentityModule.class, IdentityModule.class,
IoModule.class,
KeyAgreementModule.class, KeyAgreementModule.class,
LifecycleModule.class, LifecycleModule.class,
PluginModule.class, PluginModule.class,

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.account;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener; import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -17,6 +18,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -24,6 +26,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.fromHexString; import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.toHexString; import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -95,7 +98,7 @@ class AccountManagerImpl implements AccountManager {
} }
try { try {
BufferedReader reader = new BufferedReader(new InputStreamReader( BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8")); new FileInputStream(f), Charset.forName("UTF-8")));
String key = reader.readLine(); String key = reader.readLine();
reader.close(); reader.close();
return key; return key;
@@ -147,7 +150,7 @@ class AccountManagerImpl implements AccountManager {
@GuardedBy("stateChangeLock") @GuardedBy("stateChangeLock")
private void writeDbKeyToFile(String key, File f) throws IOException { private void writeDbKeyToFile(String key, File f) throws IOException {
FileOutputStream out = new FileOutputStream(f); FileOutputStream out = new FileOutputStream(f);
out.write(key.getBytes("UTF-8")); out.write(key.getBytes(Charset.forName("UTF-8")));
out.flush(); out.flush();
out.close(); out.close();
} }
@@ -155,8 +158,7 @@ class AccountManagerImpl implements AccountManager {
@Override @Override
public boolean accountExists() { public boolean accountExists() {
synchronized (stateChangeLock) { synchronized (stateChangeLock) {
return loadEncryptedDatabaseKey() != null return loadEncryptedDatabaseKey() != null;
&& databaseConfig.getDatabaseDirectory().isDirectory();
} }
} }
@@ -193,31 +195,24 @@ class AccountManagerImpl implements AccountManager {
} }
@Override @Override
public boolean signIn(String password) { public void signIn(String password) throws DecryptionException {
synchronized (stateChangeLock) { synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(password); databaseKey = loadAndDecryptDatabaseKey(password);
if (key == null) return false;
databaseKey = key;
return true;
} }
} }
@GuardedBy("stateChangeLock") @GuardedBy("stateChangeLock")
@Nullable private SecretKey loadAndDecryptDatabaseKey(String password)
private SecretKey loadAndDecryptDatabaseKey(String password) { throws DecryptionException {
String hex = loadEncryptedDatabaseKey(); String hex = loadEncryptedDatabaseKey();
if (hex == null) { if (hex == null) {
LOG.warning("Failed to load encrypted database key"); LOG.warning("Failed to load encrypted database key");
return null; throw new DecryptionException(INVALID_CIPHERTEXT);
} }
byte[] ciphertext = fromHexString(hex); byte[] ciphertext = fromHexString(hex);
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener(); KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password, byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
keyStrengthener); keyStrengthener);
if (plaintext == null) {
LOG.info("Failed to decrypt database key");
return null;
}
SecretKey key = new SecretKey(plaintext); SecretKey key = new SecretKey(plaintext);
// If the DB key was encrypted with a weak key and a key strengthener // If the DB key was encrypted with a weak key and a key strengthener
// is now available, re-encrypt the DB key with a strengthened key // is now available, re-encrypt the DB key with a strengthened key
@@ -230,10 +225,11 @@ class AccountManagerImpl implements AccountManager {
} }
@Override @Override
public boolean changePassword(String oldPassword, String newPassword) { public void changePassword(String oldPassword, String newPassword)
throws DecryptionException {
synchronized (stateChangeLock) { synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(oldPassword); SecretKey key = loadAndDecryptDatabaseKey(oldPassword);
return key != null && encryptAndStoreDatabaseKey(key, newPassword); encryptAndStoreDatabaseKey(key, newPassword);
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import net.i2p.crypto.eddsa.KeyPairGenerator;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey; import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey; import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser; import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.KeyStrengthener; import org.briarproject.bramble.api.crypto.KeyStrengthener;
@@ -39,6 +40,9 @@ import static java.lang.System.arraycopy;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT; import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE; import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES; import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@@ -359,16 +363,17 @@ class CryptoComponentImpl implements CryptoComponent {
} }
@Override @Override
@Nullable
public byte[] decryptWithPassword(byte[] input, String password, public byte[] decryptWithPassword(byte[] input, String password,
@Nullable KeyStrengthener keyStrengthener) { @Nullable KeyStrengthener keyStrengthener)
throws DecryptionException {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher(); AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
int macBytes = cipher.getMacBytes(); int macBytes = cipher.getMacBytes();
// The input contains the format version, salt, cost parameter, IV, // The input contains the format version, salt, cost parameter, IV,
// ciphertext and MAC // ciphertext and MAC
if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES
+ STORAGE_IV_BYTES + macBytes) + STORAGE_IV_BYTES + macBytes) {
return null; // Invalid input throw new DecryptionException(INVALID_CIPHERTEXT);
}
int inputOff = 0; int inputOff = 0;
// Format version // Format version
byte formatVersion = input[inputOff]; byte formatVersion = input[inputOff];
@@ -376,7 +381,7 @@ class CryptoComponentImpl implements CryptoComponent {
// Check whether we support this format version // Check whether we support this format version
if (formatVersion != PBKDF_FORMAT_SCRYPT && if (formatVersion != PBKDF_FORMAT_SCRYPT &&
formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) { formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
return null; throw new DecryptionException(INVALID_CIPHERTEXT);
} }
// Salt // Salt
byte[] salt = new byte[PBKDF_SALT_BYTES]; byte[] salt = new byte[PBKDF_SALT_BYTES];
@@ -385,8 +390,9 @@ class CryptoComponentImpl implements CryptoComponent {
// Cost parameter // Cost parameter
long cost = ByteUtils.readUint32(input, inputOff); long cost = ByteUtils.readUint32(input, inputOff);
inputOff += INT_32_BYTES; inputOff += INT_32_BYTES;
if (cost < 2 || cost > Integer.MAX_VALUE) if (cost < 2 || cost > Integer.MAX_VALUE) {
return null; // Invalid cost parameter throw new DecryptionException(INVALID_CIPHERTEXT);
}
// IV // IV
byte[] iv = new byte[STORAGE_IV_BYTES]; byte[] iv = new byte[STORAGE_IV_BYTES];
arraycopy(input, inputOff, iv, 0, iv.length); arraycopy(input, inputOff, iv, 0, iv.length);
@@ -394,8 +400,10 @@ class CryptoComponentImpl implements CryptoComponent {
// Derive the decryption key from the password // Derive the decryption key from the password
SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost); SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost);
if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) { if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
if (keyStrengthener == null || !keyStrengthener.isInitialised()) if (keyStrengthener == null || !keyStrengthener.isInitialised()) {
return null; // Can't derive the same strengthened key // Can't derive the same strengthened key
throw new DecryptionException(KEY_STRENGTHENER_ERROR);
}
key = keyStrengthener.strengthenKey(key); key = keyStrengthener.strengthenKey(key);
} }
// Initialise the cipher // Initialise the cipher
@@ -411,7 +419,7 @@ class CryptoComponentImpl implements CryptoComponent {
cipher.process(input, inputOff, inputLen, output, 0); cipher.process(input, inputOff, inputLen, output, 0);
return output; return output;
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
return null; // Invalid ciphertext throw new DecryptionException(INVALID_PASSWORD);
} }
} }

View File

@@ -25,6 +25,7 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose; import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir; import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
/** /**
@@ -69,8 +70,9 @@ class H2Database extends JdbcDatabase {
LOG.info("Contents of account directory before opening DB:"); LOG.info("Contents of account directory before opening DB:");
logFileOrDir(LOG, INFO, dir.getParentFile()); logFileOrDir(LOG, INFO, dir.getParentFile());
} }
boolean reopen = !dir.mkdirs(); boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen); if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.h2.Driver", reopen, key, listener); super.open("org.h2.Driver", reopen, key, listener);
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("Contents of account directory after opening DB:"); LOG.info("Contents of account directory after opening DB:");

View File

@@ -20,9 +20,11 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose; import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
/** /**
* Contains all the HSQLDB-specific code for the database. * Contains all the HSQLDB-specific code for the database.
@@ -64,7 +66,10 @@ class HyperSqlDatabase extends JdbcDatabase {
public boolean open(SecretKey key, @Nullable MigrationListener listener) public boolean open(SecretKey key, @Nullable MigrationListener listener)
throws DbException { throws DbException {
this.key = key; this.key = key;
boolean reopen = !config.getDatabaseDirectory().mkdirs(); File dir = config.getDatabaseDirectory();
boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.hsqldb.jdbc.JDBCDriver", reopen, key, listener); super.open("org.hsqldb.jdbc.JDBCDriver", reopen, key, listener);
return reopen; return reopen;
} }

View File

@@ -0,0 +1,18 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class IoModule {
@Provides
@Singleton
TimeoutMonitor provideTimeoutMonitor(TimeoutMonitorImpl timeoutMonitor) {
return timeoutMonitor;
}
}

View File

@@ -0,0 +1,104 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.concurrent.GuardedBy;
@NotNullByDefault
class TimeoutInputStream extends InputStream {
private final Clock clock;
private final InputStream in;
private final long timeoutMs;
private final CloseListener listener;
private final Object lock = new Object();
@GuardedBy("lock")
private long readStartedMs = -1;
TimeoutInputStream(Clock clock, InputStream in, long timeoutMs,
CloseListener listener) {
this.clock = clock;
this.in = in;
this.timeoutMs = timeoutMs;
this.listener = listener;
}
@Override
public int read() throws IOException {
synchronized (lock) {
readStartedMs = clock.currentTimeMillis();
}
int input = in.read();
synchronized (lock) {
readStartedMs = -1;
}
return input;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
synchronized (lock) {
readStartedMs = clock.currentTimeMillis();
}
int read = in.read(b, off, len);
synchronized (lock) {
readStartedMs = -1;
}
return read;
}
@Override
public void close() throws IOException {
try {
in.close();
} finally {
listener.onClose(this);
}
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public boolean markSupported() {
return in.markSupported();
}
@Override
public void reset() throws IOException {
in.reset();
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
boolean hasTimedOut() {
synchronized (lock) {
return readStartedMs != -1 &&
clock.currentTimeMillis() - readStartedMs > timeoutMs;
}
}
interface CloseListener {
void onClose(TimeoutInputStream closed);
}
}

View File

@@ -0,0 +1,96 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
class TimeoutMonitorImpl implements TimeoutMonitor {
private static final Logger LOG =
getLogger(TimeoutMonitorImpl.class.getName());
private static final long CHECK_INTERVAL_MS = SECONDS.toMillis(10);
private final ScheduledExecutorService scheduler;
private final Executor ioExecutor;
private final Clock clock;
private final Object lock = new Object();
@GuardedBy("lock")
private final List<TimeoutInputStream> streams = new ArrayList<>();
@GuardedBy("lock")
private Future<?> task = null;
@Inject
TimeoutMonitorImpl(@Scheduler ScheduledExecutorService scheduler,
@IoExecutor Executor ioExecutor, Clock clock) {
this.scheduler = scheduler;
this.ioExecutor = ioExecutor;
this.clock = clock;
}
@Override
public InputStream createTimeoutInputStream(InputStream in,
long timeoutMs) {
TimeoutInputStream stream = new TimeoutInputStream(clock, in,
timeoutMs, this::removeStream);
synchronized (lock) {
if (streams.isEmpty()) {
task = scheduler.scheduleWithFixedDelay(this::checkTimeouts,
CHECK_INTERVAL_MS, CHECK_INTERVAL_MS, MILLISECONDS);
}
streams.add(stream);
}
return stream;
}
private void removeStream(TimeoutInputStream stream) {
Future<?> toCancel = null;
synchronized (lock) {
if (streams.remove(stream) && streams.isEmpty()) {
toCancel = task;
task = null;
}
}
if (toCancel != null) toCancel.cancel(false);
}
@Scheduler
private void checkTimeouts() {
ioExecutor.execute(() -> {
List<TimeoutInputStream> snapshot;
synchronized (lock) {
snapshot = new ArrayList<>(streams);
}
for (TimeoutInputStream stream : snapshot) {
if (stream.hasTimedOut()) {
LOG.info("Input stream has timed out");
try {
stream.close();
} catch (IOException e) {
logException(LOG, INFO, e);
}
}
}
});
}
}

View File

@@ -15,6 +15,8 @@ import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter; import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory; import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.api.transport.KeyManager;
@@ -52,6 +54,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final HandshakeManager handshakeManager; private final HandshakeManager handshakeManager;
private final ContactExchangeManager contactExchangeManager; private final ContactExchangeManager contactExchangeManager;
private final ConnectionRegistry connectionRegistry; private final ConnectionRegistry connectionRegistry;
private final TransportPropertyManager transportPropertyManager;
@Inject @Inject
ConnectionManagerImpl(@IoExecutor Executor ioExecutor, ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
@@ -60,7 +63,8 @@ class ConnectionManagerImpl implements ConnectionManager {
SyncSessionFactory syncSessionFactory, SyncSessionFactory syncSessionFactory,
HandshakeManager handshakeManager, HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager, ContactExchangeManager contactExchangeManager,
ConnectionRegistry connectionRegistry) { ConnectionRegistry connectionRegistry,
TransportPropertyManager transportPropertyManager) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.keyManager = keyManager; this.keyManager = keyManager;
this.streamReaderFactory = streamReaderFactory; this.streamReaderFactory = streamReaderFactory;
@@ -69,6 +73,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.handshakeManager = handshakeManager; this.handshakeManager = handshakeManager;
this.contactExchangeManager = contactExchangeManager; this.contactExchangeManager = contactExchangeManager;
this.connectionRegistry = connectionRegistry; this.connectionRegistry = connectionRegistry;
this.transportPropertyManager = transportPropertyManager;
} }
@Override @Override
@@ -125,8 +130,8 @@ class ConnectionManagerImpl implements ConnectionManager {
TransportConnectionWriter w) throws IOException { TransportConnectionWriter w) throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter( StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx); w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId()); return syncSessionFactory.createSimplexOutgoingSession(
return syncSessionFactory.createSimplexOutgoingSession(c, requireNonNull(ctx.getContactId()), ctx.getTransportId(),
w.getMaxLatency(), streamWriter); w.getMaxLatency(), streamWriter);
} }
@@ -134,8 +139,8 @@ class ConnectionManagerImpl implements ConnectionManager {
TransportConnectionWriter w) throws IOException { TransportConnectionWriter w) throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter( StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx); w.getOutputStream(), ctx);
ContactId c = requireNonNull(ctx.getContactId()); return syncSessionFactory.createDuplexOutgoingSession(
return syncSessionFactory.createDuplexOutgoingSession(c, requireNonNull(ctx.getContactId()), ctx.getTransportId(),
w.getMaxLatency(), w.getMaxIdleTime(), streamWriter); w.getMaxLatency(), w.getMaxIdleTime(), streamWriter);
} }
@@ -269,6 +274,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportId transportId; private final TransportId transportId;
private final TransportConnectionReader reader; private final TransportConnectionReader reader;
private final TransportConnectionWriter writer; private final TransportConnectionWriter writer;
private final TransportProperties remote;
@Nullable @Nullable
private volatile SyncSession outgoingSession = null; private volatile SyncSession outgoingSession = null;
@@ -278,6 +284,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.transportId = transportId; this.transportId = transportId;
reader = connection.getReader(); reader = connection.getReader();
writer = connection.getWriter(); writer = connection.getWriter();
remote = connection.getRemoteProperties();
} }
@Override @Override
@@ -313,13 +320,16 @@ class ConnectionManagerImpl implements ConnectionManager {
// Start the outgoing session on another thread // Start the outgoing session on another thread
ioExecutor.execute(() -> runOutgoingSession(contactId)); ioExecutor.execute(() -> runOutgoingSession(contactId));
try { try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// Create and run the incoming session // Create and run the incoming session
createIncomingSession(ctx, reader).run(); createIncomingSession(ctx, reader).run();
reader.dispose(false, true); reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly // Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession; SyncSession out = outgoingSession;
if (out != null) out.interrupt(); if (out != null) out.interrupt();
} catch (IOException e) { } catch (DbException | IOException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
onReadError(true); onReadError(true);
} finally { } finally {
@@ -375,6 +385,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportId transportId; private final TransportId transportId;
private final TransportConnectionReader reader; private final TransportConnectionReader reader;
private final TransportConnectionWriter writer; private final TransportConnectionWriter writer;
private final TransportProperties remote;
@Nullable @Nullable
private volatile SyncSession outgoingSession = null; private volatile SyncSession outgoingSession = null;
@@ -385,6 +396,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.transportId = transportId; this.transportId = transportId;
reader = connection.getReader(); reader = connection.getReader();
writer = connection.getWriter(); writer = connection.getWriter();
remote = connection.getRemoteProperties();
} }
@Override @Override
@@ -461,13 +473,16 @@ class ConnectionManagerImpl implements ConnectionManager {
connectionRegistry.registerConnection(contactId, transportId, connectionRegistry.registerConnection(contactId, transportId,
false); false);
try { try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// Create and run the incoming session // Create and run the incoming session
createIncomingSession(ctx, reader).run(); createIncomingSession(ctx, reader).run();
reader.dispose(false, true); reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly // Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession; SyncSession out = outgoingSession;
if (out != null) out.interrupt(); if (out != null) out.interrupt();
} catch (IOException e) { } catch (DbException | IOException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
onReadError(); onReadError();
} finally { } finally {

View File

@@ -3,9 +3,30 @@ package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
@NotNullByDefault @NotNullByDefault
interface BluetoothConnectionLimiter { interface BluetoothConnectionLimiter {
/**
* How long a connection must remain open before it's considered stable.
*/
long STABILITY_PERIOD_MS = SECONDS.toMillis(90);
/**
* The minimum interval between attempts to raise the connection limit.
* This is longer than {@link #STABILITY_PERIOD_MS} so we don't start
* another attempt before knowing the outcome of the last one.
*/
long MIN_ATTEMPT_INTERVAL_MS = MINUTES.toMillis(2);
/**
* The maximum interval between attempts to raise the connection limit.
*/
long MAX_ATTEMPT_INTERVAL_MS = DAYS.toMillis(2);
/** /**
* Informs the limiter that key agreement has started. * Informs the limiter that key agreement has started.
*/ */
@@ -23,12 +44,12 @@ interface BluetoothConnectionLimiter {
boolean canOpenContactConnection(); boolean canOpenContactConnection();
/** /**
* Informs the limiter that a contact connection has been opened. The * Informs the limiter that a contact connection has been opened.
* limiter may close the new connection if key agreement is in progress.
* <p/> * <p/>
* Returns false if the limiter has closed the new connection. * Returns true if the connection is allowed.
*/ */
boolean contactConnectionOpened(DuplexTransportConnection conn); boolean contactConnectionOpened(DuplexTransportConnection conn,
boolean incoming);
/** /**
* Informs the limiter that a key agreement connection has been opened. * Informs the limiter that a key agreement connection has been opened.
@@ -37,11 +58,13 @@ interface BluetoothConnectionLimiter {
/** /**
* Informs the limiter that the given connection has been closed. * Informs the limiter that the given connection has been closed.
*
* @param exception True if the connection was closed due to an exception.
*/ */
void connectionClosed(DuplexTransportConnection conn); void connectionClosed(DuplexTransportConnection conn, boolean exception);
/** /**
* Informs the limiter that all connections have been closed. * Informs the limiter that the Bluetooth adapter has been disabled.
*/ */
void allConnectionsClosed(); void bluetoothDisabled();
} }

View File

@@ -1,46 +1,59 @@
package org.briarproject.bramble.plugin.bluetooth; package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException; import java.util.Iterator;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.lang.Math.min;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
@NotNullByDefault @NotNullByDefault
@ThreadSafe @ThreadSafe
class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter { class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(BluetoothConnectionLimiterImpl.class.getName()); getLogger(BluetoothConnectionLimiterImpl.class.getName());
private final EventBus eventBus;
private final Clock clock;
private final Object lock = new Object(); private final Object lock = new Object();
// The following are locking: lock @GuardedBy("lock")
private final LinkedList<DuplexTransportConnection> connections = private final List<ConnectionRecord> connections = new LinkedList<>();
new LinkedList<>(); @GuardedBy("lock")
private boolean keyAgreementInProgress = false; private boolean keyAgreementInProgress = false;
@GuardedBy("lock")
private int connectionLimit = 1;
@GuardedBy("lock")
private long timeOfLastAttempt = 0,
attemptInterval = MIN_ATTEMPT_INTERVAL_MS;
@Inject
BluetoothConnectionLimiterImpl(EventBus eventBus, Clock clock) {
this.eventBus = eventBus;
this.clock = clock;
}
@Override @Override
public void keyAgreementStarted() { public void keyAgreementStarted() {
List<DuplexTransportConnection> close;
synchronized (lock) { synchronized (lock) {
keyAgreementInProgress = true; keyAgreementInProgress = true;
close = new ArrayList<>(connections);
connections.clear();
} }
if (LOG.isLoggable(INFO)) { LOG.info("Key agreement started");
LOG.info("Key agreement started, closing " + close.size() + eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
" connections");
}
for (DuplexTransportConnection conn : close) tryToClose(conn);
} }
@Override @Override
@@ -55,62 +68,128 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
public boolean canOpenContactConnection() { public boolean canOpenContactConnection() {
synchronized (lock) { synchronized (lock) {
if (keyAgreementInProgress) { if (keyAgreementInProgress) {
LOG.info("Can't open contact connection during key agreement"); LOG.info("Refusing contact connection during key agreement");
return false; return false;
} else { } else {
LOG.info("Can open contact connection"); long now = clock.currentTimeMillis();
return true; return isContactConnectionAllowedByLimit(now);
} }
} }
} }
@Override @Override
public boolean contactConnectionOpened(DuplexTransportConnection conn) { public boolean contactConnectionOpened(DuplexTransportConnection conn,
boolean accept = true; boolean incoming) {
synchronized (lock) { synchronized (lock) {
if (keyAgreementInProgress) { if (keyAgreementInProgress) {
LOG.info("Refusing contact connection during key agreement"); LOG.info("Refusing contact connection during key agreement");
accept = false; return false;
} else { } else {
LOG.info("Accepting contact connection"); long now = clock.currentTimeMillis();
connections.add(conn); if (incoming || isContactConnectionAllowedByLimit(now)) {
connections.add(new ConnectionRecord(conn, now));
if (!incoming && connections.size() > connectionLimit) {
LOG.info("Attempting to raise connection limit");
timeOfLastAttempt = now;
}
return true;
} else {
return false;
}
} }
} }
if (!accept) tryToClose(conn);
return accept;
} }
@Override @Override
public void keyAgreementConnectionOpened(DuplexTransportConnection conn) { public void keyAgreementConnectionOpened(DuplexTransportConnection conn) {
synchronized (lock) { synchronized (lock) {
LOG.info("Accepting key agreement connection"); LOG.info("Accepting key agreement connection");
connections.add(conn); connections.add(
} new ConnectionRecord(conn, clock.currentTimeMillis()));
}
private void tryToClose(DuplexTransportConnection conn) {
try {
conn.getWriter().dispose(false);
conn.getReader().dispose(false, false);
} catch (IOException e) {
logException(LOG, WARNING, e);
} }
} }
@Override @Override
public void connectionClosed(DuplexTransportConnection conn) { public void connectionClosed(DuplexTransportConnection conn,
boolean exception) {
synchronized (lock) { synchronized (lock) {
connections.remove(conn); Iterator<ConnectionRecord> it = connections.iterator();
while (it.hasNext()) {
if (it.next().connection == conn) {
long now = clock.currentTimeMillis();
if (exception) connectionFailed(now);
else considerRaisingConnectionLimit(now);
it.remove();
break;
}
}
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connection closed, " + connections.size() + " open"); LOG.info("Connection closed, " + connections.size() + " open");
} }
} }
@Override @Override
public void allConnectionsClosed() { public void bluetoothDisabled() {
synchronized (lock) { synchronized (lock) {
LOG.info("Bluetooth disabled");
considerRaisingConnectionLimit(clock.currentTimeMillis());
connections.clear(); connections.clear();
LOG.info("All connections closed"); }
}
@GuardedBy("lock")
private boolean isContactConnectionAllowedByLimit(long now) {
considerRaisingConnectionLimit(now);
if (connections.size() > connectionLimit) {
LOG.info("Refusing contact connection, above limit");
return false;
} else if (connections.size() < connectionLimit) {
LOG.info("Allowing contact connection, below limit");
return true;
} else if (now - timeOfLastAttempt >= attemptInterval) {
LOG.info("Allowing contact connection, at limit");
return true;
} else {
LOG.info("Refusing contact connection, at limit");
return false;
}
}
@GuardedBy("lock")
private void considerRaisingConnectionLimit(long now) {
int stable = 0;
for (ConnectionRecord rec : connections) {
if (now - rec.timeOpened >= STABILITY_PERIOD_MS) stable++;
}
if (stable > connectionLimit) {
LOG.info("Raising connection limit");
connectionLimit = stable;
attemptInterval = MIN_ATTEMPT_INTERVAL_MS;
}
if (LOG.isLoggable(INFO)) {
LOG.info(stable + " connections are stable, limit is "
+ connectionLimit);
}
}
@GuardedBy("lock")
private void connectionFailed(long now) {
if (connections.size() > connectionLimit &&
now - timeOfLastAttempt < STABILITY_PERIOD_MS) {
LOG.info("Connection failed above limit, increasing interval");
attemptInterval = min(attemptInterval * 2, MAX_ATTEMPT_INTERVAL_MS);
}
}
private static final class ConnectionRecord {
private final DuplexTransportConnection connection;
private final long timeOpened;
private ConnectionRecord(DuplexTransportConnection connection,
long timeOpened) {
this.connection = connection;
this.timeOpened = timeOpened;
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection; import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
@@ -60,12 +61,13 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
getLogger(BluetoothPlugin.class.getName()); getLogger(BluetoothPlugin.class.getName());
final BluetoothConnectionLimiter connectionLimiter; final BluetoothConnectionLimiter connectionLimiter;
final TimeoutMonitor timeoutMonitor;
private final Executor ioExecutor; private final Executor ioExecutor;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final Backoff backoff; private final Backoff backoff;
private final PluginCallback callback; private final PluginCallback callback;
private final int maxLatency; private final int maxLatency, maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false, contactConnections = false; private volatile boolean running = false, contactConnections = false;
@@ -105,14 +107,17 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
abstract DuplexTransportConnection discoverAndConnect(String uuid); abstract DuplexTransportConnection discoverAndConnect(String uuid);
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
Executor ioExecutor, SecureRandom secureRandom, TimeoutMonitor timeoutMonitor, Executor ioExecutor,
Backoff backoff, PluginCallback callback, int maxLatency) { SecureRandom secureRandom, Backoff backoff,
PluginCallback callback, int maxLatency, int maxIdleTime) {
this.connectionLimiter = connectionLimiter; this.connectionLimiter = connectionLimiter;
this.timeoutMonitor = timeoutMonitor;
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.backoff = backoff; this.backoff = backoff;
this.callback = callback; this.callback = callback;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
} }
void onAdapterEnabled() { void onAdapterEnabled() {
@@ -125,7 +130,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
void onAdapterDisabled() { void onAdapterDisabled() {
LOG.info("Bluetooth disabled"); LOG.info("Bluetooth disabled");
tryToClose(socket); tryToClose(socket);
connectionLimiter.allConnectionsClosed(); connectionLimiter.bluetoothDisabled();
callback.transportDisabled(); callback.transportDisabled();
} }
@@ -141,8 +146,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
@Override @Override
public int getMaxIdleTime() { public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives return maxIdleTime;
return Integer.MAX_VALUE;
} }
@Override @Override
@@ -227,13 +231,26 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
if (LOG.isLoggable(INFO)) LOG.info(e.toString()); if (LOG.isLoggable(INFO)) LOG.info(e.toString());
return; return;
} }
backoff.reset(); LOG.info("Connection received");
if (connectionLimiter.contactConnectionOpened(conn)) if (connectionLimiter.contactConnectionOpened(conn, true)) {
backoff.reset();
callback.handleConnection(conn); callback.handleConnection(conn);
} else {
tryToClose(conn);
}
if (!running) return; if (!running) return;
} }
} }
private void tryToClose(DuplexTransportConnection conn) {
try {
conn.getWriter().dispose(false);
conn.getReader().dispose(false, false);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
@Override @Override
public void stop() { public void stop() {
running = false; running = false;
@@ -273,13 +290,10 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
String uuid = p.get(PROP_UUID); String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return; if (isNullOrEmpty(uuid)) return;
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (!isRunning() || !shouldAllowContactConnections()) return;
if (!connectionLimiter.canOpenContactConnection()) return;
DuplexTransportConnection d = createConnection(p); DuplexTransportConnection d = createConnection(p);
if (d != null) { if (d != null) {
backoff.reset(); backoff.reset();
if (connectionLimiter.contactConnectionOpened(d)) h.handleConnection(d);
h.handleConnection(d);
} }
}); });
} }
@@ -325,8 +339,12 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
if (isNullOrEmpty(uuid)) return null; if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid); DuplexTransportConnection conn = connect(address, uuid);
if (conn == null) return null; if (conn == null) return null;
// TODO: Why don't we reset the backoff here? if (connectionLimiter.contactConnectionOpened(conn, false)) {
return connectionLimiter.contactConnectionOpened(conn) ? conn : null; return conn;
} else {
tryToClose(conn);
return null;
}
} }
@Override @Override

View File

@@ -17,18 +17,20 @@ import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.lang.Integer.parseInt;
import static java.util.Collections.addAll; import static java.util.Collections.addAll;
import static java.util.Collections.emptyList;
import static java.util.Collections.sort; import static java.util.Collections.sort;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -37,6 +39,7 @@ import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TR
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID; import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_LAN_IP_PORTS; import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_LAN_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IP_PORTS; import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_PORT;
import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED; import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress; import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -47,15 +50,36 @@ class LanTcpPlugin extends TcpPlugin {
private static final Logger LOG = getLogger(LanTcpPlugin.class.getName()); private static final Logger LOG = getLogger(LanTcpPlugin.class.getName());
private static final LanAddressComparator ADDRESS_COMPARATOR =
new LanAddressComparator();
private static final int MAX_ADDRESSES = 4; private static final int MAX_ADDRESSES = 4;
private static final String SEPARATOR = ","; private static final String SEPARATOR = ",";
/**
* The IP address of an Android device providing a wifi access point.
*/
protected static final InetAddress WIFI_AP_ADDRESS;
/**
* The IP address of an Android device providing a wifi direct
* legacy mode access point.
*/
protected static final InetAddress WIFI_DIRECT_AP_ADDRESS;
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 43, 1});
WIFI_DIRECT_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 49, 1});
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
LanTcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback, LanTcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) { int maxLatency, int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime); super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
} }
@Override @Override
@@ -64,37 +88,81 @@ class LanTcpPlugin extends TcpPlugin {
} }
@Override @Override
protected List<InetSocketAddress> getLocalSocketAddresses() { public void start() {
// Use the same address and port as last time if available if (used.getAndSet(true)) throw new IllegalStateException();
initialisePortProperty();
running = true;
bind();
}
protected void initialisePortProperty() {
TransportProperties p = callback.getLocalProperties(); TransportProperties p = callback.getLocalProperties();
if (isNullOrEmpty(p.get(PROP_PORT))) {
int port = new Random().nextInt(32768) + 32768;
p.put(PROP_PORT, String.valueOf(port));
callback.mergeLocalProperties(p);
}
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses() {
TransportProperties p = callback.getLocalProperties();
int preferredPort = parsePortProperty(p.get(PROP_PORT));
String oldIpPorts = p.get(PROP_IP_PORTS); String oldIpPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts); List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
List<InetSocketAddress> locals = new ArrayList<>(); List<InetSocketAddress> locals = new ArrayList<>();
for (InetAddress local : getLocalIpAddresses()) { List<InetSocketAddress> fallbacks = new ArrayList<>();
if (isAcceptableAddress(local)) { for (InetAddress local : getUsableLocalInetAddresses()) {
// If this is the old address, try to use the same port // If we've used this address before, try to use the same port
for (InetSocketAddress old : olds) { int port = preferredPort;
if (old.getAddress().equals(local)) for (InetSocketAddress old : olds) {
locals.add(new InetSocketAddress(local, old.getPort())); if (old.getAddress().equals(local)) {
port = old.getPort();
break;
} }
locals.add(new InetSocketAddress(local, 0));
} }
locals.add(new InetSocketAddress(local, port));
// Fall back to any available port
fallbacks.add(new InetSocketAddress(local, 0));
} }
sort(locals, ADDRESS_COMPARATOR); locals.addAll(fallbacks);
return locals; return locals;
} }
private int parsePortProperty(@Nullable String portProperty) {
if (isNullOrEmpty(portProperty)) return 0;
try {
return parseInt(portProperty);
} catch (NumberFormatException e) {
return 0;
}
}
private List<InetSocketAddress> parseSocketAddresses(String ipPorts) { private List<InetSocketAddress> parseSocketAddresses(String ipPorts) {
if (isNullOrEmpty(ipPorts)) return emptyList();
String[] split = ipPorts.split(SEPARATOR);
List<InetSocketAddress> addresses = new ArrayList<>(); List<InetSocketAddress> addresses = new ArrayList<>();
for (String ipPort : split) { if (isNullOrEmpty(ipPorts)) return addresses;
for (String ipPort : ipPorts.split(SEPARATOR)) {
InetSocketAddress a = parseSocketAddress(ipPort); InetSocketAddress a = parseSocketAddress(ipPort);
if (a != null) addresses.add(a); if (a != null) addresses.add(a);
} }
return addresses; return addresses;
} }
protected List<InetAddress> getUsableLocalInetAddresses() {
List<InterfaceAddress> ifAddrs =
new ArrayList<>(getLocalInterfaceAddresses());
// Prefer longer network prefixes
sort(ifAddrs, (a, b) ->
b.getNetworkPrefixLength() - a.getNetworkPrefixLength());
List<InetAddress> addrs = new ArrayList<>();
for (InterfaceAddress ifAddr : ifAddrs) {
InetAddress addr = ifAddr.getAddress();
if (isAcceptableAddress(addr)) addrs.add(addr);
}
return addrs;
}
@Override @Override
protected void setLocalSocketAddress(InetSocketAddress a) { protected void setLocalSocketAddress(InetSocketAddress a) {
String ipPort = getIpPortString(a); String ipPort = getIpPortString(a);
@@ -132,7 +200,20 @@ class LanTcpPlugin extends TcpPlugin {
@Override @Override
protected List<InetSocketAddress> getRemoteSocketAddresses( protected List<InetSocketAddress> getRemoteSocketAddresses(
TransportProperties p) { TransportProperties p) {
return parseSocketAddresses(p.get(PROP_IP_PORTS)); String ipPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> remotes = parseSocketAddresses(ipPorts);
int port = parsePortProperty(p.get(PROP_PORT));
// If the contact has a preferred port, we can guess their IP:port when
// they're providing a wifi access point
if (port != 0) {
InetSocketAddress wifiAp =
new InetSocketAddress(WIFI_AP_ADDRESS, port);
if (!remotes.contains(wifiAp)) remotes.add(wifiAp);
InetSocketAddress wifiDirectAp =
new InetSocketAddress(WIFI_DIRECT_AP_ADDRESS, port);
if (!remotes.contains(wifiDirectAp)) remotes.add(wifiDirectAp);
}
return remotes;
} }
private boolean isAcceptableAddress(InetAddress a) { private boolean isAcceptableAddress(InetAddress a) {
@@ -145,52 +226,33 @@ class LanTcpPlugin extends TcpPlugin {
} }
@Override @Override
protected boolean isConnectable(InetSocketAddress remote) { protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false; if (remote.getPort() == 0) return false;
if (!isAcceptableAddress(remote.getAddress())) return false; if (!isAcceptableAddress(remote.getAddress())) return false;
// Try to determine whether the address is on the same LAN as us // Try to determine whether the address is on the same LAN as us
if (socket == null) return false; byte[] localIp = local.getAddress().getAddress();
byte[] localIp = socket.getInetAddress().getAddress();
byte[] remoteIp = remote.getAddress().getAddress(); byte[] remoteIp = remote.getAddress().getAddress();
return addressesAreOnSameLan(localIp, remoteIp); int prefixLength = local.getNetworkPrefixLength();
return areAddressesInSameNetwork(localIp, remoteIp, prefixLength);
} }
// Package access for testing // Package access for testing
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) { static boolean areAddressesInSameNetwork(byte[] localIp, byte[] remoteIp,
// 10.0.0.0/8 int prefixLength) {
if (isPrefix10(localIp)) return isPrefix10(remoteIp); if (localIp.length != remoteIp.length) return false;
// 172.16.0.0/12 // Compare the first prefixLength bits of the addresses
if (isPrefix172(localIp)) return isPrefix172(remoteIp); for (int i = 0; i < prefixLength; i++) {
// 192.168.0.0/16 int byteIndex = i >> 3;
if (isPrefix192(localIp)) return isPrefix192(remoteIp); int bitIndex = i & 7; // 0 to 7
// Unrecognised prefix - may be compatible int mask = 128 >> bitIndex; // Select the bit at bitIndex
if ((localIp[byteIndex] & mask) != (remoteIp[byteIndex] & mask)) {
return false; // Addresses differ at bit i
}
}
return true; return true;
} }
private static boolean isPrefix10(byte[] ipv4) {
return ipv4[0] == 10;
}
private static boolean isPrefix172(byte[] ipv4) {
return ipv4[0] == (byte) 172 && (ipv4[1] & 0xF0) == 16;
}
private static boolean isPrefix192(byte[] ipv4) {
return ipv4[0] == (byte) 192 && ipv4[1] == (byte) 168;
}
// Returns the prefix length for an RFC 1918 address, or 0 for any other
// address
private static int getRfc1918PrefixLength(InetAddress addr) {
if (!(addr instanceof Inet4Address)) return 0;
if (!addr.isSiteLocalAddress()) return 0;
byte[] ipv4 = addr.getAddress();
if (isPrefix10(ipv4)) return 8;
if (isPrefix172(ipv4)) return 12;
if (isPrefix192(ipv4)) return 16;
return 0;
}
@Override @Override
public boolean supportsKeyAgreement() { public boolean supportsKeyAgreement() {
return true; return true;
@@ -229,6 +291,12 @@ class LanTcpPlugin extends TcpPlugin {
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) { byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null; if (!isRunning()) return null;
ServerSocket ss = socket;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for key agreement server socket");
return null;
}
InetSocketAddress remote; InetSocketAddress remote;
try { try {
remote = parseSocketAddress(descriptor); remote = parseSocketAddress(descriptor);
@@ -236,12 +304,11 @@ class LanTcpPlugin extends TcpPlugin {
LOG.info("Invalid IP/port in key agreement descriptor"); LOG.info("Invalid IP/port in key agreement descriptor");
return null; return null;
} }
if (!isConnectable(remote)) { if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) + LOG.info(scrubSocketAddress(remote) +
" is not connectable from " + " is not connectable from " +
scrubSocketAddress(local)); scrubSocketAddress(ss.getLocalSocketAddress()));
} }
return null; return null;
} }
@@ -249,8 +316,8 @@ class LanTcpPlugin extends TcpPlugin {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote)); LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket(); Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0)); s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote); s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout); s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote)); LOG.info("Connected to " + scrubSocketAddress(remote));
@@ -299,19 +366,4 @@ class LanTcpPlugin extends TcpPlugin {
IoUtils.tryToClose(ss, LOG, WARNING); IoUtils.tryToClose(ss, LOG, WARNING);
} }
} }
static class LanAddressComparator implements Comparator<InetSocketAddress> {
@Override
public int compare(InetSocketAddress a, InetSocketAddress b) {
// Prefer addresses with non-zero ports
int aPort = a.getPort(), bPort = b.getPort();
if (aPort > 0 && bPort == 0) return -1;
if (aPort == 0 && bPort > 0) return 1;
// Prefer addresses with longer RFC 1918 prefixes
int aPrefix = getRfc1918PrefixLength(a.getAddress());
int bPrefix = getRfc1918PrefixLength(b.getAddress());
return bPrefix - aPrefix;
}
}
} }

View File

@@ -18,10 +18,11 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault @NotNullByDefault
public class LanTcpPluginFactory implements DuplexPluginFactory { public class LanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
@@ -48,6 +49,6 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY, return new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY,
MAX_IDLE_TIME); MAX_IDLE_TIME, CONNECTION_TIMEOUT);
} }
} }

View File

@@ -19,10 +19,10 @@ import org.briarproject.bramble.util.IoUtils;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -36,7 +36,6 @@ import java.util.regex.Pattern;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.list; import static java.util.Collections.list;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
@@ -58,7 +57,8 @@ abstract class TcpPlugin implements DuplexPlugin {
protected final Executor ioExecutor, bindExecutor; protected final Executor ioExecutor, bindExecutor;
protected final Backoff backoff; protected final Backoff backoff;
protected final PluginCallback callback; protected final PluginCallback callback;
protected final int maxLatency, maxIdleTime, socketTimeout; protected final int maxLatency, maxIdleTime;
protected final int connectionTimeout, socketTimeout;
protected final AtomicBoolean used = new AtomicBoolean(false); protected final AtomicBoolean used = new AtomicBoolean(false);
protected volatile boolean running = false; protected volatile boolean running = false;
@@ -86,15 +86,18 @@ abstract class TcpPlugin implements DuplexPlugin {
/** /**
* Returns true if connections to the given address can be attempted. * Returns true if connections to the given address can be attempted.
*/ */
protected abstract boolean isConnectable(InetSocketAddress remote); @SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected abstract boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote);
TcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback, TcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) { int maxLatency, int maxIdleTime, int connectionTimeout) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.backoff = backoff; this.backoff = backoff;
this.callback = callback; this.callback = callback;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime; this.maxIdleTime = maxIdleTime;
this.connectionTimeout = connectionTimeout;
if (maxIdleTime > Integer.MAX_VALUE / 2) if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE; socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2; else socketTimeout = maxIdleTime * 2;
@@ -230,13 +233,23 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override @Override
public DuplexTransportConnection createConnection(TransportProperties p) { public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null; if (!isRunning()) return null;
ServerSocket ss = socket;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for server socket");
return null;
}
for (InetSocketAddress remote : getRemoteSocketAddresses(p)) { for (InetSocketAddress remote : getRemoteSocketAddresses(p)) {
if (!isConnectable(remote)) { // Don't try to connect to our own address
if (!canConnectToOwnAddress() &&
remote.getAddress().equals(ss.getInetAddress())) {
continue;
}
if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) + LOG.info(scrubSocketAddress(remote) +
" is not connectable from " + " is not connectable from " +
scrubSocketAddress(local)); scrubSocketAddress(ss.getLocalSocketAddress()));
} }
continue; continue;
} }
@@ -244,8 +257,8 @@ abstract class TcpPlugin implements DuplexPlugin {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote)); LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket(); Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0)); s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote); s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout); s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote)); LOG.info("Connected to " + scrubSocketAddress(remote));
@@ -259,6 +272,19 @@ abstract class TcpPlugin implements DuplexPlugin {
return null; return null;
} }
@Nullable
InterfaceAddress getLocalInterfaceAddress(InetAddress a) {
for (InterfaceAddress ifAddr : getLocalInterfaceAddresses()) {
if (ifAddr.getAddress().equals(a)) return ifAddr;
}
return null;
}
// Override for testing
protected boolean canConnectToOwnAddress() {
return false;
}
protected Socket createSocket() throws IOException { protected Socket createSocket() throws IOException {
return new Socket(); return new Socket();
} }
@@ -314,14 +340,27 @@ abstract class TcpPlugin implements DuplexPlugin {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
Collection<InetAddress> getLocalIpAddresses() { List<InterfaceAddress> getLocalInterfaceAddresses() {
List<InterfaceAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(iface.getInterfaceAddresses());
}
return addrs;
}
List<InetAddress> getLocalInetAddresses() {
List<InetAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(list(iface.getInetAddresses()));
}
return addrs;
}
private List<NetworkInterface> getNetworkInterfaces() {
try { try {
Enumeration<NetworkInterface> ifaces = getNetworkInterfaces(); Enumeration<NetworkInterface> ifaces =
if (ifaces == null) return emptyList(); NetworkInterface.getNetworkInterfaces();
List<InetAddress> addrs = new ArrayList<>(); return ifaces == null ? emptyList() : list(ifaces);
for (NetworkInterface iface : list(ifaces))
addrs.addAll(list(iface.getInetAddresses()));
return addrs;
} catch (SocketException e) { } catch (SocketException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
return emptyList(); return emptyList();

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.properties.TransportProperties;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -29,8 +30,10 @@ class WanTcpPlugin extends TcpPlugin {
private volatile MappingResult mappingResult; private volatile MappingResult mappingResult;
WanTcpPlugin(Executor ioExecutor, Backoff backoff, PortMapper portMapper, WanTcpPlugin(Executor ioExecutor, Backoff backoff, PortMapper portMapper,
PluginCallback callback, int maxLatency, int maxIdleTime) { PluginCallback callback, int maxLatency, int maxIdleTime,
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime); int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
this.portMapper = portMapper; this.portMapper = portMapper;
} }
@@ -45,7 +48,7 @@ class WanTcpPlugin extends TcpPlugin {
TransportProperties p = callback.getLocalProperties(); TransportProperties p = callback.getLocalProperties();
InetSocketAddress old = parseSocketAddress(p.get(PROP_IP_PORT)); InetSocketAddress old = parseSocketAddress(p.get(PROP_IP_PORT));
List<InetSocketAddress> addrs = new LinkedList<>(); List<InetSocketAddress> addrs = new LinkedList<>();
for (InetAddress a : getLocalIpAddresses()) { for (InetAddress a : getLocalInetAddresses()) {
if (isAcceptableAddress(a)) { if (isAcceptableAddress(a)) {
// If this is the old address, try to use the same port // If this is the old address, try to use the same port
if (old != null && old.getAddress().equals(a)) if (old != null && old.getAddress().equals(a))
@@ -86,7 +89,8 @@ class WanTcpPlugin extends TcpPlugin {
} }
@Override @Override
protected boolean isConnectable(InetSocketAddress remote) { protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false; if (remote.getPort() == 0) return false;
return isAcceptableAddress(remote.getAddress()); return isAcceptableAddress(remote.getAddress());
} }

View File

@@ -19,10 +19,11 @@ import static org.briarproject.bramble.api.plugin.WanTcpConstants.ID;
@NotNullByDefault @NotNullByDefault
public class WanTcpPluginFactory implements DuplexPluginFactory { public class WanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int CONNECTION_TIMEOUT = 30_000; // 30 seconds
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
@@ -52,6 +53,6 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new WanTcpPlugin(ioExecutor, backoff, return new WanTcpPlugin(ioExecutor, backoff,
new PortMapperImpl(shutdownManager), callback, MAX_LATENCY, new PortMapperImpl(shutdownManager), callback, MAX_LATENCY,
MAX_IDLE_TIME); MAX_IDLE_TIME, CONNECTION_TIMEOUT);
} }
} }

View File

@@ -37,6 +37,11 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.GROUP_KEY_DISCOVERED;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class TransportPropertyManagerImpl implements TransportPropertyManager, class TransportPropertyManagerImpl implements TransportPropertyManager,
@@ -111,10 +116,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
try { try {
// Find the latest update for this transport, if any // Find the latest update for this transport, if any
BdfDictionary d = metadataParser.parse(meta); BdfDictionary d = metadataParser.parse(meta);
TransportId t = new TransportId(d.getString("transportId")); TransportId t = new TransportId(d.getString(MSG_KEY_TRANSPORT_ID));
LatestUpdate latest = findLatest(txn, m.getGroupId(), t, false); LatestUpdate latest = findLatest(txn, m.getGroupId(), t, false);
if (latest != null) { if (latest != null) {
if (d.getLong("version") > latest.version) { if (d.getLong(MSG_KEY_VERSION) > latest.version) {
// This update is newer - delete the previous update // This update is newer - delete the previous update
db.deleteMessage(txn, latest.messageId); db.deleteMessage(txn, latest.messageId);
db.deleteMessageMetadata(txn, latest.messageId); db.deleteMessageMetadata(txn, latest.messageId);
@@ -140,6 +145,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
} }
} }
@Override
public void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException {
if (props.isEmpty()) return;
try {
db.transaction(false, txn -> {
Group g = getContactGroup(db.getContact(txn, c));
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(
txn, g.getId());
BdfDictionary discovered =
meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (discovered == null) discovered = new BdfDictionary();
discovered.putAll(props);
meta.put(GROUP_KEY_DISCOVERED, discovered);
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
});
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override @Override
public Map<TransportId, TransportProperties> getLocalProperties() public Map<TransportId, TransportProperties> getLocalProperties()
throws DbException { throws DbException {
@@ -203,12 +229,26 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
Group g = getContactGroup(c); Group g = getContactGroup(c);
try { try {
// Find the latest remote update // Find the latest remote update
TransportProperties remote;
LatestUpdate latest = findLatest(txn, g.getId(), t, false); LatestUpdate latest = findLatest(txn, g.getId(), t, false);
if (latest == null) return new TransportProperties(); if (latest == null) {
// Retrieve and parse the latest remote properties remote = new TransportProperties();
BdfList message = } else {
clientHelper.getMessageAsList(txn, latest.messageId); // Retrieve and parse the latest remote properties
return parseProperties(message); BdfList message =
clientHelper.getMessageAsList(txn, latest.messageId);
remote = parseProperties(message);
}
// Merge in any discovered properties
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
BdfDictionary d = meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (d == null) return remote;
TransportProperties merged =
clientHelper.parseAndValidateTransportProperties(d);
// Received properties override discovered properties
merged.putAll(remote);
return merged;
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }
@@ -281,9 +321,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
Message m = clientHelper.createMessage(g, now, body); Message m = clientHelper.createMessage(g, now, body);
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put("transportId", t.getString()); meta.put(MSG_KEY_TRANSPORT_ID, t.getString());
meta.put("version", version); meta.put(MSG_KEY_VERSION, version);
meta.put("local", local); meta.put(MSG_KEY_LOCAL, local);
clientHelper.addLocalMessage(txn, m, meta, shared, false); clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -302,8 +342,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
.getMessageMetadataAsDictionary(txn, localGroup.getId()); .getMessageMetadataAsDictionary(txn, localGroup.getId());
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) { for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue(); BdfDictionary meta = e.getValue();
TransportId t = new TransportId(meta.getString("transportId")); TransportId t =
long version = meta.getLong("version"); new TransportId(meta.getString(MSG_KEY_TRANSPORT_ID));
long version = meta.getLong(MSG_KEY_VERSION);
latestUpdates.put(t, new LatestUpdate(e.getKey(), version)); latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
} }
return latestUpdates; return latestUpdates;
@@ -316,9 +357,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
clientHelper.getMessageMetadataAsDictionary(txn, g); clientHelper.getMessageMetadataAsDictionary(txn, g);
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) { for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue(); BdfDictionary meta = e.getValue();
if (meta.getString("transportId").equals(t.getString()) if (meta.getString(MSG_KEY_TRANSPORT_ID).equals(t.getString())
&& meta.getBoolean("local") == local) { && meta.getBoolean(MSG_KEY_LOCAL) == local) {
return new LatestUpdate(e.getKey(), meta.getLong("version")); return new LatestUpdate(e.getKey(),
meta.getLong(MSG_KEY_VERSION));
} }
} }
return null; return null;

View File

@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.Offer; import org.briarproject.bramble.api.sync.Offer;
@@ -18,6 +19,7 @@ import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions; import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent; import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent; import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent; import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
@@ -71,6 +73,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private final EventBus eventBus; private final EventBus eventBus;
private final Clock clock; private final Clock clock;
private final ContactId contactId; private final ContactId contactId;
private final TransportId transportId;
private final int maxLatency, maxIdleTime; private final int maxLatency, maxIdleTime;
private final StreamWriter streamWriter; private final StreamWriter streamWriter;
private final SyncRecordWriter recordWriter; private final SyncRecordWriter recordWriter;
@@ -86,14 +89,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private volatile boolean interrupted = false; private volatile boolean interrupted = false;
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor, DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, Clock clock, ContactId contactId, int maxLatency, EventBus eventBus, Clock clock, ContactId contactId,
int maxIdleTime, StreamWriter streamWriter, TransportId transportId, int maxLatency, int maxIdleTime,
SyncRecordWriter recordWriter) { StreamWriter streamWriter, SyncRecordWriter recordWriter) {
this.db = db; this.db = db;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.eventBus = eventBus; this.eventBus = eventBus;
this.clock = clock; this.clock = clock;
this.contactId = contactId; this.contactId = contactId;
this.transportId = transportId;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime; this.maxIdleTime = maxIdleTime;
this.streamWriter = streamWriter; this.streamWriter = streamWriter;
@@ -223,6 +227,9 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
} else if (e instanceof LifecycleEvent) { } else if (e instanceof LifecycleEvent) {
LifecycleEvent l = (LifecycleEvent) e; LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt(); if (l.getLifecycleState() == STOPPING) interrupt();
} else if (e instanceof CloseSyncConnectionsEvent) {
CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e;
if (c.getTransportId().equals(transportId)) interrupt();
} }
} }

View File

@@ -11,11 +11,13 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions; import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.transport.StreamWriter; import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.IOException; import java.io.IOException;
@@ -56,6 +58,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
private final Executor dbExecutor; private final Executor dbExecutor;
private final EventBus eventBus; private final EventBus eventBus;
private final ContactId contactId; private final ContactId contactId;
private final TransportId transportId;
private final int maxLatency; private final int maxLatency;
private final StreamWriter streamWriter; private final StreamWriter streamWriter;
private final SyncRecordWriter recordWriter; private final SyncRecordWriter recordWriter;
@@ -65,12 +68,14 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
private volatile boolean interrupted = false; private volatile boolean interrupted = false;
SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor, SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, ContactId contactId, int maxLatency, EventBus eventBus, ContactId contactId, TransportId transportId,
StreamWriter streamWriter, SyncRecordWriter recordWriter) { int maxLatency, StreamWriter streamWriter,
SyncRecordWriter recordWriter) {
this.db = db; this.db = db;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.eventBus = eventBus; this.eventBus = eventBus;
this.contactId = contactId; this.contactId = contactId;
this.transportId = transportId;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
this.streamWriter = streamWriter; this.streamWriter = streamWriter;
this.recordWriter = recordWriter; this.recordWriter = recordWriter;
@@ -123,6 +128,9 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
} else if (e instanceof LifecycleEvent) { } else if (e instanceof LifecycleEvent) {
LifecycleEvent l = (LifecycleEvent) e; LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt(); if (l.getLifecycleState() == STOPPING) interrupt();
} else if (e instanceof CloseSyncConnectionsEvent) {
CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e;
if (c.getTransportId().equals(transportId)) interrupt();
} }
} }

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.SyncRecordReader; import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.SyncRecordReaderFactory; import org.briarproject.bramble.api.sync.SyncRecordReaderFactory;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
@@ -53,22 +54,23 @@ class SyncSessionFactoryImpl implements SyncSessionFactory {
} }
@Override @Override
public SyncSession createSimplexOutgoingSession(ContactId c, public SyncSession createSimplexOutgoingSession(ContactId c, TransportId t,
int maxLatency, StreamWriter streamWriter) { int maxLatency, StreamWriter streamWriter) {
OutputStream out = streamWriter.getOutputStream(); OutputStream out = streamWriter.getOutputStream();
SyncRecordWriter recordWriter = SyncRecordWriter recordWriter =
recordWriterFactory.createRecordWriter(out); recordWriterFactory.createRecordWriter(out);
return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, t,
maxLatency, streamWriter, recordWriter); maxLatency, streamWriter, recordWriter);
} }
@Override @Override
public SyncSession createDuplexOutgoingSession(ContactId c, int maxLatency, public SyncSession createDuplexOutgoingSession(ContactId c,
int maxIdleTime, StreamWriter streamWriter) { TransportId t, int maxLatency, int maxIdleTime,
StreamWriter streamWriter) {
OutputStream out = streamWriter.getOutputStream(); OutputStream out = streamWriter.getOutputStream();
SyncRecordWriter recordWriter = SyncRecordWriter recordWriter =
recordWriterFactory.createRecordWriter(out); recordWriterFactory.createRecordWriter(out);
return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c, return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c, t,
maxLatency, maxIdleTime, streamWriter, recordWriter); maxLatency, maxIdleTime, streamWriter, recordWriter);
} }
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.account; package org.briarproject.bramble.account;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener; import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -19,12 +20,15 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getIdentity; import static org.briarproject.bramble.test.TestUtils.getIdentity;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -35,6 +39,7 @@ import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class AccountManagerImplTest extends BrambleMockTestCase { public class AccountManagerImplTest extends BrambleMockTestCase {
@@ -83,8 +88,13 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
} }
@Test @Test
public void testSignInReturnsFalseIfDbKeyCannotBeLoaded() { public void testSignInThrowsExceptionIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.signIn(password)); try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey()); assertFalse(accountManager.hasDatabaseKey());
assertFalse(keyFile.exists()); assertFalse(keyFile.exists());
@@ -92,11 +102,11 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
} }
@Test @Test
public void testSignInReturnsFalseIfPasswordIsWrong() throws Exception { public void testSignInThrowsExceptionIfPasswordIsWrong() throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password, oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener); keyStrengthener);
will(returnValue(null)); will(throwException(new DecryptionException(INVALID_PASSWORD)));
}}); }});
storeDatabaseKey(keyFile, encryptedKeyHex); storeDatabaseKey(keyFile, encryptedKeyHex);
@@ -105,7 +115,12 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(accountManager.signIn(password)); try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey()); assertFalse(accountManager.hasDatabaseKey());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
@@ -128,7 +143,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password)); accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey()); assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey(); SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted); assertNotNull(decrypted);
@@ -157,7 +172,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password)); accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey()); assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey(); SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted); assertNotNull(decrypted);
@@ -239,55 +254,6 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertFalse(keyBackupFile.exists()); assertFalse(keyBackupFile.exists());
} }
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryDoesNotExist()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(dbDir.exists());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(dbDir.exists());
}
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryIsNotDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.createNewFile());
assertFalse(dbDir.isDirectory());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertFalse(dbDir.isDirectory());
}
@Test
public void testAccountExistsReturnsTrueIfDbDirectoryIsDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.mkdirs());
assertTrue(dbDir.isDirectory());
assertTrue(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertTrue(dbDir.isDirectory());
}
@Test @Test
public void testCreateAccountStoresDbKey() throws Exception { public void testCreateAccountStoresDbKey() throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -315,26 +281,36 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
} }
@Test @Test
public void testChangePasswordReturnsFalseIfDbKeyCannotBeLoaded() { public void testChangePasswordThrowsExceptionIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.changePassword(password, newPassword)); try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(keyFile.exists()); assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists()); assertFalse(keyBackupFile.exists());
} }
@Test @Test
public void testChangePasswordReturnsFalseIfPasswordIsWrong() public void testChangePasswordThrowsExceptionIfPasswordIsWrong()
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password, oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener); keyStrengthener);
will(returnValue(null)); will(throwException(new DecryptionException(INVALID_PASSWORD)));
}}); }});
storeDatabaseKey(keyFile, encryptedKeyHex); storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex); storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(accountManager.changePassword(password, newPassword)); try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile)); assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -357,7 +333,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
storeDatabaseKey(keyFile, encryptedKeyHex); storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex); storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(accountManager.changePassword(password, newPassword)); accountManager.changePassword(password, newPassword);
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile)); assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile)); assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -366,7 +342,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
private void storeDatabaseKey(File f, String hex) throws IOException { private void storeDatabaseKey(File f, String hex) throws IOException {
f.getParentFile().mkdirs(); f.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(f); FileOutputStream out = new FileOutputStream(f);
out.write(hex.getBytes("UTF-8")); out.write(hex.getBytes(Charset.forName("UTF-8")));
out.flush(); out.flush();
out.close(); out.close();
} }
@@ -374,7 +350,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
@Nullable @Nullable
private String loadDatabaseKey(File f) throws IOException { private String loadDatabaseKey(File f) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader( BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8")); new FileInputStream(f), Charset.forName("UTF-8")));
String hex = reader.readLine(); String hex = reader.readLine();
reader.close(); reader.close();
return hex; return hex;

View File

@@ -1,25 +1,35 @@
package org.briarproject.bramble.crypto; package org.briarproject.bramble.crypto;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.system.SystemClock; import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestSecureRandomProvider; import org.briarproject.bramble.test.TestSecureRandomProvider;
import org.briarproject.bramble.test.TestUtils; import org.jmock.Expectations;
import org.junit.Test; import org.junit.Test;
import java.util.Random; import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class PasswordBasedEncryptionTest extends BrambleTestCase { public class PasswordBasedEncryptionTest extends BrambleMockTestCase {
private final KeyStrengthener keyStrengthener =
context.mock(KeyStrengthener.class);
private final CryptoComponentImpl crypto = private final CryptoComponentImpl crypto =
new CryptoComponentImpl(new TestSecureRandomProvider(), new CryptoComponentImpl(new TestSecureRandomProvider(),
new ScryptKdf(new SystemClock())); new ScryptKdf(new SystemClock()));
@Test @Test
public void testEncryptionAndDecryption() { public void testEncryptionAndDecryption() throws Exception {
byte[] input = TestUtils.getRandomBytes(1234); byte[] input = getRandomBytes(1234);
String password = "password"; String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null); byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
byte[] output = crypto.decryptWithPassword(ciphertext, password, null); byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
@@ -27,14 +37,80 @@ public class PasswordBasedEncryptionTest extends BrambleTestCase {
} }
@Test @Test
public void testInvalidCiphertextReturnsNull() { public void testInvalidFormatVersionThrowsException() {
byte[] input = TestUtils.getRandomBytes(1234); byte[] input = getRandomBytes(1234);
String password = "password"; String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null); byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
// Modify the ciphertext
int position = new Random().nextInt(ciphertext.length); // Modify the format version
ciphertext[position] = (byte) (ciphertext[position] ^ 0xFF); ciphertext[0] ^= (byte) 0xFF;
byte[] output = crypto.decryptWithPassword(ciphertext, password, null); try {
assertNull(output); crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
}
@Test
public void testInvalidPasswordThrowsException() {
byte[] input = getRandomBytes(1234);
byte[] ciphertext = crypto.encryptWithPassword(input, "password", null);
// Try to decrypt with the wrong password
try {
crypto.decryptWithPassword(ciphertext, "wrong", null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
}
@Test
public void testMissingKeyStrengthenerThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener is missing during decryption
try {
crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
}
@Test
public void testKeyStrengthenerFailureThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
oneOf(keyStrengthener).isInitialised();
will(returnValue(false));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener fails during decryption
try {
crypto.decryptWithPassword(ciphertext, password, keyStrengthener);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
} }
} }

View File

@@ -0,0 +1,143 @@
package org.briarproject.bramble.io;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.SettableClock;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class TimeoutInputStreamTest extends BrambleTestCase {
private static final long TIMEOUT_MS = MINUTES.toMillis(1);
private final long now = System.currentTimeMillis();
private AtomicLong time;
private UnresponsiveInputStream in;
private AtomicBoolean listenerCalled;
private TimeoutInputStream stream;
private CountDownLatch readReturned;
@Before
public void setUp() {
time = new AtomicLong(now);
in = new UnresponsiveInputStream();
listenerCalled = new AtomicBoolean(false);
stream = new TimeoutInputStream(new SettableClock(time), in,
TIMEOUT_MS, stream -> listenerCalled.set(true));
readReturned = new CountDownLatch(1);
}
@Test
public void testTimeoutIsReportedIfReadDoesNotReturn() throws Exception {
startReading();
try {
// The stream should not report a timeout
assertFalse(stream.hasTimedOut());
// Time passes
time.set(now + TIMEOUT_MS);
// The stream still shouldn't report a timeout
assertFalse(stream.hasTimedOut());
// Time passes
time.set(now + TIMEOUT_MS + 1);
// The stream should report a timeout
assertTrue(stream.hasTimedOut());
// The listener should not have been called yet
assertFalse(listenerCalled.get());
// Close the stream
stream.close();
// The listener should have been called
assertTrue(listenerCalled.get());
} finally {
// Allow the read to return
in.readFinished.countDown();
}
}
@Test
public void testTimeoutIsNotReportedIfReadReturns() throws Exception {
startReading();
try {
// The stream should not report a timeout
assertFalse(stream.hasTimedOut());
// Time passes
time.set(now + TIMEOUT_MS);
// The stream still shouldn't report a timeout
assertFalse(stream.hasTimedOut());
// Allow the read to finish and wait for it to return
in.readFinished.countDown();
readReturned.await(10, SECONDS);
// Time passes
time.set(now + TIMEOUT_MS + 1);
// The stream should not report a timeout as the read has returned
assertFalse(stream.hasTimedOut());
// The listener should not have been called yet
assertFalse(listenerCalled.get());
// Close the stream
stream.close();
// The listener should have been called
assertTrue(listenerCalled.get());
} finally {
// Allow the read to return in case an assertion was thrown
in.readFinished.countDown();
}
}
private void startReading() throws Exception {
// Start a background thread to read from the unresponsive stream
new Thread(() -> {
try {
assertEquals(123, stream.read());
readReturned.countDown();
} catch (IOException e) {
fail();
}
}).start();
// Wait for the background thread to start reading
assertTrue(in.readStarted.await(10, SECONDS));
}
private class UnresponsiveInputStream extends InputStream {
private final CountDownLatch readStarted = new CountDownLatch(1);
private final CountDownLatch readFinished = new CountDownLatch(1);
@Override
public int read() throws IOException {
readStarted.countDown();
try {
readFinished.await();
return 123;
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
}

View File

@@ -0,0 +1,182 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.SettableClock;
import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicLong;
import static org.briarproject.bramble.plugin.bluetooth.BluetoothConnectionLimiter.MIN_ATTEMPT_INTERVAL_MS;
import static org.briarproject.bramble.plugin.bluetooth.BluetoothConnectionLimiter.STABILITY_PERIOD_MS;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BluetoothConnectionLimiterImplTest extends BrambleMockTestCase {
private final EventBus eventBus = context.mock(EventBus.class);
private final DuplexTransportConnection conn1 =
context.mock(DuplexTransportConnection.class, "conn1");
private final DuplexTransportConnection conn2 =
context.mock(DuplexTransportConnection.class, "conn2");
private final DuplexTransportConnection conn3 =
context.mock(DuplexTransportConnection.class, "conn3");
private final long now = System.currentTimeMillis();
private AtomicLong time;
private BluetoothConnectionLimiter limiter;
@Before
public void setUp() {
time = new AtomicLong(now);
Clock clock = new SettableClock(time);
limiter = new BluetoothConnectionLimiterImpl(eventBus, clock);
}
@Test
public void testLimiterDoesNotAllowContactConnectionsDuringKeyAgreement() {
assertTrue(limiter.canOpenContactConnection());
expectCloseSyncConnectionsEvent();
limiter.keyAgreementStarted();
assertFalse(limiter.canOpenContactConnection());
limiter.keyAgreementEnded();
assertTrue(limiter.canOpenContactConnection());
}
@Test
public void testLimiterAllowsAttemptToRaiseLimitAtStartup() {
// First outgoing connection is allowed - we're below the limit of 1
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn1, false));
// Second outgoing connection is allowed - it's time to try raising
// the limit to 2
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn2, false));
// Third outgoing connection is not allowed - we're above the limit of 1
assertFalse(limiter.canOpenContactConnection());
}
@Test
public void testLimiterAllowsThirdConnectionAfterFirstTwoAreClosed() {
// First outgoing connection is allowed - we're below the limit of 1
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn1, false));
// Second outgoing connection is allowed - it's time to try raising
// the limit to 2
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn2, false));
// Third outgoing connection is not allowed - we're above the limit of 1
assertFalse(limiter.canOpenContactConnection());
// Close the first connection
limiter.connectionClosed(conn1, false);
// Third outgoing connection is not allowed - we're at the limit of 1
assertFalse(limiter.canOpenContactConnection());
// Close the second connection
limiter.connectionClosed(conn2, false);
// Third outgoing connection is allowed - we're below the limit of 1
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn3, false));
}
@Test
public void testLimiterRaisesLimitWhenConnectionsAreStable() {
// First outgoing connection is allowed - we're below the limit of 1
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn1, false));
// Second outgoing connection is allowed - it's time to try raising
// the limit to 2
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn2, false));
// Third outgoing connection is not allowed - we're above the limit of 1
assertFalse(limiter.canOpenContactConnection());
// Time passes
time.set(now + STABILITY_PERIOD_MS);
// Third outgoing connection is still not allowed - first two are now
// stable so limit is raised to 2, but we're already at the new limit
assertFalse(limiter.canOpenContactConnection());
// Time passes
time.set(now + MIN_ATTEMPT_INTERVAL_MS);
// Third outgoing connection is allowed - it's time to try raising
// the limit to 3
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn3, false));
// Fourth outgoing connection is not allowed - we're above the limit
// of 2
assertFalse(limiter.canOpenContactConnection());
}
@Test
public void testLimiterIncreasesIntervalWhenConnectionFailsAboveLimit() {
// First outgoing connection is allowed - we're below the limit of 1
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn1, false));
// Time passes
time.set(now + 1);
// Second outgoing connection is allowed - it's time to try raising
// the limit to 2
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn2, false));
// Time passes - the first connection is stable, the second isn't
time.set(now + STABILITY_PERIOD_MS);
// First connection fails. The second connection isn't stable yet, so
// the limiter considers this a failed attempt and doubles the interval
// between attempts
limiter.connectionClosed(conn1, true);
// Third outgoing connection is not allowed - we're still at the limit
// of 1
assertFalse(limiter.canOpenContactConnection());
// Time passes - nearly time for the second attempt
time.set(now + MIN_ATTEMPT_INTERVAL_MS * 2);
// Third outgoing connection is not allowed - we're still at the limit
// of 1
assertFalse(limiter.canOpenContactConnection());
// Time passes - now it's time for the second attempt
time.set(now + 1 + MIN_ATTEMPT_INTERVAL_MS * 2);
// Third outgoing connection is allowed - it's time to try raising the
// limit to 2 again
assertTrue(limiter.canOpenContactConnection());
assertTrue(limiter.contactConnectionOpened(conn3, false));
}
private void expectCloseSyncConnectionsEvent() {
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(
CloseSyncConnectionsEvent.class)));
}});
}
}

View File

@@ -7,12 +7,11 @@ import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback; import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter; import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.plugin.tcp.LanTcpPlugin.LanAddressComparator;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.IOException; import java.io.IOException;
@@ -22,7 +21,6 @@ import java.net.InetSocketAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.Comparator;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -33,56 +31,89 @@ import static java.util.concurrent.Executors.newCachedThreadPool;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
import static org.briarproject.bramble.plugin.tcp.LanTcpPlugin.areAddressesInSameNetwork;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class LanTcpPluginTest extends BrambleTestCase { public class LanTcpPluginTest extends BrambleTestCase {
private final Backoff backoff = new TestBackoff(); private final Backoff backoff = new TestBackoff();
private final ExecutorService ioExecutor = newCachedThreadPool(); private final ExecutorService ioExecutor = newCachedThreadPool();
private Callback callback = null;
private LanTcpPlugin plugin = null;
@Before
public void setUp() {
callback = new Callback();
plugin = new LanTcpPlugin(ioExecutor, backoff, callback, 0, 0, 1000) {
@Override
protected boolean canConnectToOwnAddress() {
return true;
}
};
}
@Test @Test
public void testAddressesAreOnSameLan() { public void testAreAddressesInSameNetwork() {
Callback callback = new Callback(); // Local and remote in 10.0.0.0/8
LanTcpPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback, assertTrue(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
0, 0); makeAddress(10, 255, 255, 255), 8));
// Local and remote in 10.0.0.0/8 should return true assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
assertTrue(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(10, 255, 255, 255), 9));
makeAddress(10, 255, 255, 255)));
// Local and remote in 172.16.0.0/12 should return true // Local and remote in 172.16.0.0/12
assertTrue(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), assertTrue(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(172, 31, 255, 255))); makeAddress(172, 31, 255, 255), 12));
// Local and remote in 192.168.0.0/16 should return true assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(172, 31, 255, 255), 13));
makeAddress(192, 168, 255, 255)));
// Local and remote in 169.254.0.0/16 (link-local) should return true // Local and remote in 192.168.0.0/16
assertTrue(plugin.addressesAreOnSameLan(makeAddress(169, 254, 0, 0), assertTrue(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(169, 254, 255, 255))); makeAddress(192, 168, 255, 255), 16));
// Local and remote in different recognised prefixes should return false assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(192, 168, 255, 255), 17));
makeAddress(172, 31, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), // Local and remote in 169.254.0.0/16
makeAddress(192, 168, 255, 255))); assertTrue(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(169, 254, 255, 255), 16));
makeAddress(10, 255, 255, 255))); assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(169, 254, 255, 255), 17));
makeAddress(192, 168, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), // Local in 10.0.0.0/8, remote in a different network
makeAddress(10, 255, 255, 255))); assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(172, 31, 255, 255), 8));
makeAddress(172, 31, 255, 255))); assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
// Remote prefix unrecognised should return false makeAddress(192, 168, 255, 255), 8));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(1, 2, 3, 4))); makeAddress(169, 254, 255, 255), 8));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0),
makeAddress(1, 2, 3, 4))); // Local in 172.16.0.0/12, remote in a different network
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(1, 2, 3, 4))); makeAddress(10, 255, 255, 255), 12));
// Both prefixes unrecognised should return true (could be link-local) assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
assertTrue(plugin.addressesAreOnSameLan(makeAddress(1, 2, 3, 4), makeAddress(192, 168, 255, 255), 12));
makeAddress(5, 6, 7, 8))); assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(169, 254, 255, 255), 12));
// Local in 192.168.0.0/16, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(10, 255, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(172, 31, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(169, 254, 255, 255), 16));
// Local in 169.254.0.0/16, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(10, 255, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(172, 31, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(192, 168, 255, 255), 16));
} }
private byte[] makeAddress(int... parts) { private byte[] makeAddress(int... parts) {
@@ -93,13 +124,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test @Test
public void testIncomingConnection() throws Exception { public void testIncomingConnection() throws Exception {
if (!systemHasLocalIpv4Address()) { assumeTrue(systemHasLocalIpv4Address());
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
plugin.start(); plugin.start();
// The plugin should have bound a socket and stored the port number // The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS)); assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -128,13 +153,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test @Test
public void testOutgoingConnection() throws Exception { public void testOutgoingConnection() throws Exception {
if (!systemHasLocalIpv4Address()) { assumeTrue(systemHasLocalIpv4Address());
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
plugin.start(); plugin.start();
// The plugin should have bound a socket and stored the port number // The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS)); assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -177,13 +196,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test @Test
public void testIncomingKeyAgreementConnection() throws Exception { public void testIncomingKeyAgreementConnection() throws Exception {
if (!systemHasLocalIpv4Address()) { assumeTrue(systemHasLocalIpv4Address());
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
plugin.start(); plugin.start();
assertTrue(callback.propertiesLatch.await(5, SECONDS)); assertTrue(callback.propertiesLatch.await(5, SECONDS));
KeyAgreementListener kal = KeyAgreementListener kal =
@@ -225,13 +238,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test @Test
public void testOutgoingKeyAgreementConnection() throws Exception { public void testOutgoingKeyAgreementConnection() throws Exception {
if (!systemHasLocalIpv4Address()) { assumeTrue(systemHasLocalIpv4Address());
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
plugin.start(); plugin.start();
// The plugin should have bound a socket and stored the port number // The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS)); assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -276,62 +283,12 @@ public class LanTcpPluginTest extends BrambleTestCase {
plugin.stop(); plugin.stop();
} }
@Test
public void testComparatorPrefersNonZeroPorts() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress nonZero = new InetSocketAddress("1.2.3.4", 1234);
InetSocketAddress zero = new InetSocketAddress("1.2.3.4", 0);
assertEquals(0, comparator.compare(nonZero, nonZero));
assertTrue(comparator.compare(nonZero, zero) < 0);
assertTrue(comparator.compare(zero, nonZero) > 0);
assertEquals(0, comparator.compare(zero, zero));
}
@Test
public void testComparatorPrefersLongerPrefixes() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
assertEquals(0, comparator.compare(prefix192, prefix192));
assertTrue(comparator.compare(prefix192, prefix172) < 0);
assertTrue(comparator.compare(prefix192, prefix10) < 0);
assertTrue(comparator.compare(prefix172, prefix192) > 0);
assertEquals(0, comparator.compare(prefix172, prefix172));
assertTrue(comparator.compare(prefix172, prefix10) < 0);
assertTrue(comparator.compare(prefix10, prefix192) > 0);
assertTrue(comparator.compare(prefix10, prefix172) > 0);
assertEquals(0, comparator.compare(prefix10, prefix10));
}
@Test
public void testComparatorPrefersSiteLocalToLinkLocal() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
InetSocketAddress linkLocal = new InetSocketAddress("169.254.0.1", 0);
assertTrue(comparator.compare(prefix192, linkLocal) < 0);
assertTrue(comparator.compare(prefix172, linkLocal) < 0);
assertTrue(comparator.compare(prefix10, linkLocal) < 0);
assertTrue(comparator.compare(linkLocal, prefix192) > 0);
assertTrue(comparator.compare(linkLocal, prefix172) > 0);
assertTrue(comparator.compare(linkLocal, prefix10) > 0);
assertEquals(0, comparator.compare(linkLocal, linkLocal));
}
private boolean systemHasLocalIpv4Address() throws Exception { private boolean systemHasLocalIpv4Address() throws Exception {
for (NetworkInterface i : list(getNetworkInterfaces())) { for (NetworkInterface i : list(getNetworkInterfaces())) {
for (InetAddress a : list(i.getInetAddresses())) { for (InetAddress a : list(i.getInetAddresses())) {
if (a instanceof Inet4Address) if (a instanceof Inet4Address) {
return a.isLinkLocalAddress() || a.isSiteLocalAddress(); return a.isLinkLocalAddress() || a.isSiteLocalAddress();
}
} }
} }
return false; return false;
@@ -340,7 +297,9 @@ public class LanTcpPluginTest extends BrambleTestCase {
@NotNullByDefault @NotNullByDefault
private static class Callback implements PluginCallback { private static class Callback implements PluginCallback {
private final CountDownLatch propertiesLatch = new CountDownLatch(1); // Properties will be stored twice: the preferred port at startup,
// and the IP:port when the server socket is bound
private final CountDownLatch propertiesLatch = new CountDownLatch(2);
private final CountDownLatch connectionsLatch = new CountDownLatch(1); private final CountDownLatch connectionsLatch = new CountDownLatch(1);
private final TransportProperties local = new TransportProperties(); private final TransportProperties local = new TransportProperties();

View File

@@ -24,14 +24,18 @@ import org.briarproject.bramble.test.DbExpectations;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.junit.Test; import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.GROUP_KEY_DISCOVERED;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID; import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION; import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -186,25 +190,25 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Message message = getMessage(contactGroupId); Message message = getMessage(contactGroupId);
Metadata meta = new Metadata(); Metadata meta = new Metadata();
BdfDictionary metaDictionary = BdfDictionary.of( BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 2), new BdfEntry(MSG_KEY_VERSION, 2),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
); );
Map<MessageId, BdfDictionary> messageMetadata = Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>(); new LinkedHashMap<>();
// A remote update for another transport should be ignored // A remote update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId()); MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of( messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
)); ));
// A local update for the same transport should be ignored // A local update for the same transport should be ignored
MessageId localUpdateId = new MessageId(getRandomId()); MessageId localUpdateId = new MessageId(getRandomId());
messageMetadata.put(localUpdateId, BdfDictionary.of( messageMetadata.put(localUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -228,18 +232,18 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Metadata meta = new Metadata(); Metadata meta = new Metadata();
// Version 4 is being delivered // Version 4 is being delivered
BdfDictionary metaDictionary = BdfDictionary.of( BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 4), new BdfEntry(MSG_KEY_VERSION, 4),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
); );
Map<MessageId, BdfDictionary> messageMetadata = Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>(); new LinkedHashMap<>();
// An older remote update for the same transport should be deleted // An older remote update for the same transport should be deleted
MessageId fooVersion3 = new MessageId(getRandomId()); MessageId fooVersion3 = new MessageId(getRandomId());
messageMetadata.put(fooVersion3, BdfDictionary.of( messageMetadata.put(fooVersion3, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 3), new BdfEntry(MSG_KEY_VERSION, 3),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
)); ));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -265,18 +269,18 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Metadata meta = new Metadata(); Metadata meta = new Metadata();
// Version 3 is being delivered // Version 3 is being delivered
BdfDictionary metaDictionary = BdfDictionary.of( BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 3), new BdfEntry(MSG_KEY_VERSION, 3),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
); );
Map<MessageId, BdfDictionary> messageMetadata = Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>(); new LinkedHashMap<>();
// A newer remote update for the same transport should not be deleted // A newer remote update for the same transport should not be deleted
MessageId fooVersion4 = new MessageId(getRandomId()); MessageId fooVersion4 = new MessageId(getRandomId());
messageMetadata.put(fooVersion4, BdfDictionary.of( messageMetadata.put(fooVersion4, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 4), new BdfEntry(MSG_KEY_VERSION, 4),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
)); ));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -342,9 +346,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// A local update for another transport should be ignored // A local update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId()); MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of( messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
@@ -366,16 +370,16 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// A local update for another transport should be ignored // A local update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId()); MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of( messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
// A local update for the right transport should be returned // A local update for the right transport should be returned
MessageId fooUpdateId = new MessageId(getRandomId()); MessageId fooUpdateId = new MessageId(getRandomId());
messageMetadata.put(fooUpdateId, BdfDictionary.of( messageMetadata.put(fooUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict); BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict);
@@ -405,28 +409,28 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
List<Contact> contacts = asList(contact1, contact2); List<Contact> contacts = asList(contact1, contact2);
Group contactGroup1 = getGroup(CLIENT_ID, MAJOR_VERSION); Group contactGroup1 = getGroup(CLIENT_ID, MAJOR_VERSION);
Group contactGroup2 = getGroup(CLIENT_ID, MAJOR_VERSION); Group contactGroup2 = getGroup(CLIENT_ID, MAJOR_VERSION);
Map<MessageId, BdfDictionary> messageMetadata2 = Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>(); new LinkedHashMap<>();
// A remote update for another transport should be ignored // A remote update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId()); MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata2.put(barUpdateId, BdfDictionary.of( messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
)); ));
// A local update for the right transport should be ignored // A local update for the right transport should be ignored
MessageId localUpdateId = new MessageId(getRandomId()); MessageId localUpdateId = new MessageId(getRandomId());
messageMetadata2.put(localUpdateId, BdfDictionary.of( messageMetadata.put(localUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
// A remote update for the right transport should be returned // A remote update for the right transport should be returned
MessageId fooUpdateId = new MessageId(getRandomId()); MessageId fooUpdateId = new MessageId(getRandomId());
messageMetadata2.put(fooUpdateId, BdfDictionary.of( messageMetadata.put(fooUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", false) new BdfEntry(MSG_KEY_LOCAL, false)
)); ));
BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict); BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict);
@@ -440,19 +444,25 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(contactGroup1)); will(returnValue(contactGroup1));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup1.getId()); contactGroup1.getId());
will(returnValue(Collections.emptyMap())); will(returnValue(emptyMap()));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup1.getId());
will(returnValue(new BdfDictionary()));
// Second contact: returns an update // Second contact: returns an update
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact2); MAJOR_VERSION, contact2);
will(returnValue(contactGroup2)); will(returnValue(contactGroup2));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup2.getId()); contactGroup2.getId());
will(returnValue(messageMetadata2)); will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId); oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
will(returnValue(fooProperties)); will(returnValue(fooProperties));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup2.getId());
will(returnValue(new BdfDictionary()));
}}); }});
TransportPropertyManagerImpl t = createInstance(); TransportPropertyManagerImpl t = createInstance();
@@ -463,6 +473,62 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
assertEquals(fooProperties, properties.get(contact2.getId())); assertEquals(fooProperties, properties.get(contact2.getId()));
} }
@Test
public void testReceivePropertiesOverrideDiscoveredProperties()
throws Exception {
Transaction txn = new Transaction(null, true);
Contact contact = getContact();
List<Contact> contacts = singletonList(contact);
Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
MessageId updateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> messageMetadata = singletonMap(updateId,
BdfDictionary.of(
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, false)
));
BdfList update = BdfList.of("foo", 1, fooPropertiesDict);
TransportProperties discovered = new TransportProperties();
discovered.put("fooKey1", "overridden");
discovered.put("fooKey3", "fooValue3");
BdfDictionary discoveredDict = new BdfDictionary(discovered);
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_DISCOVERED, discoveredDict)
);
TransportProperties merged = new TransportProperties();
merged.putAll(fooProperties);
merged.put("fooKey3", "fooValue3");
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getContacts(txn);
will(returnValue(contacts));
// One update
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
oneOf(clientHelper).parseAndValidateTransportProperties(
discoveredDict);
will(returnValue(discovered));
}});
TransportPropertyManagerImpl t = createInstance();
Map<ContactId, TransportProperties> properties =
t.getRemoteProperties(new TransportId("foo"));
assertEquals(merged, properties.get(contact.getId()));
}
@Test @Test
public void testMergingUnchangedPropertiesDoesNotCreateUpdate() public void testMergingUnchangedPropertiesDoesNotCreateUpdate()
throws Exception { throws Exception {
@@ -470,9 +536,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
MessageId updateId = new MessageId(getRandomId()); MessageId updateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> messageMetadata = singletonMap(updateId, Map<MessageId, BdfDictionary> messageMetadata = singletonMap(updateId,
BdfDictionary.of( BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
)); ));
BdfList update = BdfList.of("foo", 1, fooPropertiesDict); BdfList update = BdfList.of("foo", 1, fooPropertiesDict);
@@ -505,7 +571,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// There are no existing properties to merge with // There are no existing properties to merge with
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
will(returnValue(Collections.emptyMap())); will(returnValue(emptyMap()));
// Store the new properties in the local group, version 1 // Store the new properties in the local group, version 1
expectStoreMessage(txn, localGroup.getId(), "foo", expectStoreMessage(txn, localGroup.getId(), "foo",
fooPropertiesDict, 1, true, false); fooPropertiesDict, 1, true, false);
@@ -517,7 +583,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(contactGroup)); will(returnValue(contactGroup));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(Collections.emptyMap())); will(returnValue(emptyMap()));
expectStoreMessage(txn, contactGroup.getId(), "foo", expectStoreMessage(txn, contactGroup.getId(), "foo",
fooPropertiesDict, 1, true, true); fooPropertiesDict, 1, true, true);
}}); }});
@@ -532,9 +598,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact = getContact(); Contact contact = getContact();
Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION); Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
BdfDictionary oldMetadata = BdfDictionary.of( BdfDictionary oldMetadata = BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 1), new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry("local", true) new BdfEntry(MSG_KEY_LOCAL, true)
); );
MessageId localGroupUpdateId = new MessageId(getRandomId()); MessageId localGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> localGroupMessageMetadata = Map<MessageId, BdfDictionary> localGroupMessageMetadata =
@@ -589,14 +655,14 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// The latest update for transport "foo" should be returned // The latest update for transport "foo" should be returned
MessageId fooVersion999 = new MessageId(getRandomId()); MessageId fooVersion999 = new MessageId(getRandomId());
messageMetadata.put(fooVersion999, BdfDictionary.of( messageMetadata.put(fooVersion999, BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry("version", 999) new BdfEntry(MSG_KEY_VERSION, 999)
)); ));
// The latest update for transport "bar" should be returned // The latest update for transport "bar" should be returned
MessageId barVersion3 = new MessageId(getRandomId()); MessageId barVersion3 = new MessageId(getRandomId());
messageMetadata.put(barVersion3, BdfDictionary.of( messageMetadata.put(barVersion3, BdfDictionary.of(
new BdfEntry("transportId", "bar"), new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry("version", 3) new BdfEntry(MSG_KEY_VERSION, 3)
)); ));
BdfList fooUpdate = BdfList.of("foo", 999, fooPropertiesDict); BdfList fooUpdate = BdfList.of("foo", 999, fooPropertiesDict);
BdfList barUpdate = BdfList.of("bar", 3, barPropertiesDict); BdfList barUpdate = BdfList.of("bar", 3, barPropertiesDict);
@@ -627,9 +693,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Message message = getMessage(g); Message message = getMessage(g);
long timestamp = message.getTimestamp(); long timestamp = message.getTimestamp();
BdfDictionary meta = BdfDictionary.of( BdfDictionary meta = BdfDictionary.of(
new BdfEntry("transportId", transportId), new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId),
new BdfEntry("version", version), new BdfEntry(MSG_KEY_VERSION, version),
new BdfEntry("local", local) new BdfEntry(MSG_KEY_LOCAL, local)
); );
context.checking(new Expectations() {{ context.checking(new Expectations() {{

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
@@ -23,6 +24,7 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.test.TestUtils.getContactId; import static org.briarproject.bramble.test.TestUtils.getContactId;
import static org.briarproject.bramble.test.TestUtils.getMessage; import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
public class SimplexOutgoingSessionTest extends BrambleMockTestCase { public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
@@ -36,14 +38,15 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
private final Executor dbExecutor = new ImmediateExecutor(); private final Executor dbExecutor = new ImmediateExecutor();
private final ContactId contactId = getContactId(); private final ContactId contactId = getContactId();
private final TransportId transportId = getTransportId();
private final Message message = getMessage(new GroupId(getRandomId())); private final Message message = getMessage(new GroupId(getRandomId()));
private final MessageId messageId = message.getId(); private final MessageId messageId = message.getId();
@Test @Test
public void testNothingToSend() throws Exception { public void testNothingToSend() throws Exception {
SimplexOutgoingSession session = new SimplexOutgoingSession(db, SimplexOutgoingSession session = new SimplexOutgoingSession(db,
dbExecutor, eventBus, contactId, MAX_LATENCY, streamWriter, dbExecutor, eventBus, contactId, transportId, MAX_LATENCY,
recordWriter); streamWriter, recordWriter);
Transaction noAckTxn = new Transaction(null, false); Transaction noAckTxn = new Transaction(null, false);
Transaction noMsgTxn = new Transaction(null, false); Transaction noMsgTxn = new Transaction(null, false);
@@ -76,8 +79,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
public void testSomethingToSend() throws Exception { public void testSomethingToSend() throws Exception {
Ack ack = new Ack(singletonList(messageId)); Ack ack = new Ack(singletonList(messageId));
SimplexOutgoingSession session = new SimplexOutgoingSession(db, SimplexOutgoingSession session = new SimplexOutgoingSession(db,
dbExecutor, eventBus, contactId, MAX_LATENCY, streamWriter, dbExecutor, eventBus, contactId, transportId, MAX_LATENCY,
recordWriter); streamWriter, recordWriter);
Transaction ackTxn = new Transaction(null, false); Transaction ackTxn = new Transaction(null, false);
Transaction noAckTxn = new Transaction(null, false); Transaction noAckTxn = new Transaction(null, false);
Transaction msgTxn = new Transaction(null, false); Transaction msgTxn = new Transaction(null, false);

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter; import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -37,6 +38,11 @@ public class TestDuplexTransportConnection
return writer; return writer;
} }
@Override
public TransportProperties getRemoteProperties() {
return new TransportProperties();
}
/** /**
* Creates and returns a pair of TestDuplexTransportConnections that are * Creates and returns a pair of TestDuplexTransportConnections that are
* connected to each other. * connected to each other.

View File

@@ -16,7 +16,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: '*.jar') implementation fileTree(dir: 'libs', include: '*.jar')
implementation 'net.java.dev.jna:jna:4.5.2' implementation 'net.java.dev.jna:jna:4.5.2'
implementation 'net.java.dev.jna:jna-platform:4.5.2' implementation 'net.java.dev.jna:jna-platform:4.5.2'
tor 'org.briarproject:tor:0.3.5.9@zip' tor 'org.briarproject:tor:0.3.5.10@zip'
tor 'org.briarproject:obfs4proxy:0.0.7@zip' tor 'org.briarproject:obfs4proxy:0.0.7@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24' annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.plugin; package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.ShutdownManager; import org.briarproject.bramble.api.lifecycle.ShutdownManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -9,6 +10,7 @@ import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory; import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory; import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory;
import org.briarproject.bramble.plugin.modem.ModemPluginFactory; import org.briarproject.bramble.plugin.modem.ModemPluginFactory;
import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory; import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory;
@@ -31,10 +33,11 @@ public class DesktopPluginModule extends PluginModule {
PluginConfig getPluginConfig(@IoExecutor Executor ioExecutor, PluginConfig getPluginConfig(@IoExecutor Executor ioExecutor,
SecureRandom random, BackoffFactory backoffFactory, SecureRandom random, BackoffFactory backoffFactory,
ReliabilityLayerFactory reliabilityFactory, ReliabilityLayerFactory reliabilityFactory,
ShutdownManager shutdownManager, EventBus eventBus) { ShutdownManager shutdownManager, EventBus eventBus, Clock clock,
DuplexPluginFactory bluetooth = TimeoutMonitor timeoutMonitor) {
new JavaBluetoothPluginFactory(ioExecutor, random, eventBus, DuplexPluginFactory bluetooth = new JavaBluetoothPluginFactory(
backoffFactory); ioExecutor, random, eventBus, clock, timeoutMonitor,
backoffFactory);
DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor, DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
reliabilityFactory); reliabilityFactory);
DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor, DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor,

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.bluetooth; package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -34,10 +35,11 @@ class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> {
private volatile LocalDevice localDevice = null; private volatile LocalDevice localDevice = null;
JavaBluetoothPlugin(BluetoothConnectionLimiter connectionManager, JavaBluetoothPlugin(BluetoothConnectionLimiter connectionManager,
Executor ioExecutor, SecureRandom secureRandom, TimeoutMonitor timeoutMonitor, Executor ioExecutor,
Backoff backoff, PluginCallback callback, int maxLatency) { SecureRandom secureRandom, Backoff backoff,
super(connectionManager, ioExecutor, secureRandom, backoff, callback, PluginCallback callback, int maxLatency, int maxIdleTime) {
maxLatency); super(connectionManager, timeoutMonitor, ioExecutor, secureRandom,
backoff, callback, maxLatency, maxIdleTime);
} }
@Override @Override
@@ -119,7 +121,9 @@ class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM"; return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
} }
private DuplexTransportConnection wrapSocket(StreamConnection s) { private DuplexTransportConnection wrapSocket(StreamConnection s)
return new JavaBluetoothTransportConnection(this, connectionLimiter, s); throws IOException {
return new JavaBluetoothTransportConnection(this, connectionLimiter,
timeoutMonitor, s);
} }
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.plugin.bluetooth; package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.BackoffFactory; import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -8,6 +9,7 @@ import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.system.Clock;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -21,22 +23,27 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
public class JavaBluetoothPluginFactory implements DuplexPluginFactory { public class JavaBluetoothPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final BackoffFactory backoffFactory;
private final EventBus eventBus; private final EventBus eventBus;
private final Clock clock;
private final TimeoutMonitor timeoutMonitor;
private final BackoffFactory backoffFactory;
public JavaBluetoothPluginFactory(Executor ioExecutor, public JavaBluetoothPluginFactory(Executor ioExecutor,
SecureRandom secureRandom, EventBus eventBus, SecureRandom secureRandom, EventBus eventBus, Clock clock,
BackoffFactory backoffFactory) { TimeoutMonitor timeoutMonitor, BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.backoffFactory = backoffFactory;
this.eventBus = eventBus; this.eventBus = eventBus;
this.clock = clock;
this.timeoutMonitor = timeoutMonitor;
this.backoffFactory = backoffFactory;
} }
@Override @Override
@@ -52,11 +59,12 @@ public class JavaBluetoothPluginFactory implements DuplexPluginFactory {
@Override @Override
public DuplexPlugin createPlugin(PluginCallback callback) { public DuplexPlugin createPlugin(PluginCallback callback) {
BluetoothConnectionLimiter connectionLimiter = BluetoothConnectionLimiter connectionLimiter =
new BluetoothConnectionLimiterImpl(); new BluetoothConnectionLimiterImpl(eventBus, clock);
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
JavaBluetoothPlugin plugin = new JavaBluetoothPlugin(connectionLimiter, JavaBluetoothPlugin plugin = new JavaBluetoothPlugin(connectionLimiter,
ioExecutor, secureRandom, backoff, callback, MAX_LATENCY); timeoutMonitor, ioExecutor, secureRandom, backoff, callback,
MAX_LATENCY, MAX_IDLE_TIME);
eventBus.addListener(plugin); eventBus.addListener(plugin);
return plugin; return plugin;
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.bluetooth; package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin; import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
@@ -14,20 +15,24 @@ import javax.microedition.io.StreamConnection;
class JavaBluetoothTransportConnection class JavaBluetoothTransportConnection
extends AbstractDuplexTransportConnection { extends AbstractDuplexTransportConnection {
private final BluetoothConnectionLimiter connectionManager; private final BluetoothConnectionLimiter connectionLimiter;
private final StreamConnection stream; private final StreamConnection stream;
private final InputStream in;
JavaBluetoothTransportConnection(Plugin plugin, JavaBluetoothTransportConnection(Plugin plugin,
BluetoothConnectionLimiter connectionManager, BluetoothConnectionLimiter connectionLimiter,
StreamConnection stream) { TimeoutMonitor timeoutMonitor,
StreamConnection stream) throws IOException {
super(plugin); super(plugin);
this.connectionLimiter = connectionLimiter;
this.stream = stream; this.stream = stream;
this.connectionManager = connectionManager; in = timeoutMonitor.createTimeoutInputStream(
stream.openInputStream(), plugin.getMaxIdleTime() * 2);
} }
@Override @Override
protected InputStream getInputStream() throws IOException { protected InputStream getInputStream() {
return stream.openInputStream(); return in;
} }
@Override @Override
@@ -40,7 +45,7 @@ class JavaBluetoothTransportConnection
try { try {
stream.close(); stream.close();
} finally { } finally {
connectionManager.connectionClosed(this); connectionLimiter.connectionClosed(this, exception);
} }
} }
} }

View File

@@ -24,7 +24,7 @@ dependencyVerification {
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8', 'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8', 'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.briarproject:obfs4proxy:0.0.7:obfs4proxy-0.0.7.zip:5b2f693262ce43a7e130f7cc7d5d1617925330640a2eb6d71085e95df8ee0642', 'org.briarproject:obfs4proxy:0.0.7:obfs4proxy-0.0.7.zip:5b2f693262ce43a7e130f7cc7d5d1617925330640a2eb6d71085e95df8ee0642',
'org.briarproject:tor:0.3.5.9:tor-0.3.5.9.zip:6c3994b129db019cc23caaf50d6b4383903c40d05fbc47fc94211170a3e5d38c', 'org.briarproject:tor:0.3.5.10:tor-0.3.5.10.zip:7b387d3523ae8af289c23be59dc4c64ec5d3721385d7825a09705095e3318d5c',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d', 'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a', 'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53', 'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',

View File

@@ -28,7 +28,9 @@ import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@RequiresApi(23) @RequiresApi(23)
@NotNullByDefault @NotNullByDefault
@@ -79,7 +81,10 @@ class AndroidKeyStrengthener implements KeyStrengthener {
return true; return true;
} }
return false; return false;
} catch (GeneralSecurityException | IOException e) { } catch (GeneralSecurityException e) {
logException(LOG, WARNING, e);
return false;
} catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.io.TimeoutMonitor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkManager;
@@ -36,6 +37,7 @@ import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog; import org.briarproject.briar.api.android.DozeWatchdog;
@@ -64,7 +66,11 @@ import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONIO
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class}) @Module(includes = {
ContactExchangeModule.class,
LoginModule.class,
ViewModelModule.class
})
public class AppModule { public class AppModule {
static class EagerSingletons { static class EagerSingletons {
@@ -117,11 +123,12 @@ public class AppModule {
LocationUtils locationUtils, EventBus eventBus, LocationUtils locationUtils, EventBus eventBus,
ResourceProvider resourceProvider, ResourceProvider resourceProvider,
CircumventionProvider circumventionProvider, CircumventionProvider circumventionProvider,
BatteryManager batteryManager, Clock clock) { BatteryManager batteryManager, Clock clock,
TimeoutMonitor timeoutMonitor) {
Context appContext = app.getApplicationContext(); Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth = DuplexPluginFactory bluetooth = new AndroidBluetoothPluginFactory(
new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor, ioExecutor, androidExecutor, appContext, random, eventBus,
appContext, random, eventBus, clock, backoffFactory); clock, timeoutMonitor, backoffFactory);
DuplexPluginFactory tor = new AndroidTorPluginFactory(ioExecutor, DuplexPluginFactory tor = new AndroidTorPluginFactory(ioExecutor,
scheduler, appContext, networkManager, locationUtils, eventBus, scheduler, appContext, networkManager, locationUtils, eventBus,
torSocketFactory, backoffFactory, resourceProvider, torSocketFactory, backoffFactory, resourceProvider,

View File

@@ -8,8 +8,6 @@ import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.BriarControllerImpl; import org.briarproject.briar.android.controller.BriarControllerImpl;
import org.briarproject.briar.android.controller.DbController; import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.DbControllerImpl; import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.login.ChangePasswordController;
import org.briarproject.briar.android.login.ChangePasswordControllerImpl;
import org.briarproject.briar.android.navdrawer.NavDrawerController; import org.briarproject.briar.android.navdrawer.NavDrawerController;
import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl; import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
@@ -46,13 +44,6 @@ public class ActivityModule {
return setupController; return setupController;
} }
@ActivityScope
@Provides
ChangePasswordController providePasswordController(
ChangePasswordControllerImpl passwordController) {
return passwordController;
}
@ActivityScope @ActivityScope
@Provides @Provides
protected BriarController provideBriarController( protected BriarController provideBriarController(
@@ -80,5 +71,4 @@ public class ActivityModule {
BriarServiceConnection provideBriarServiceConnection() { BriarServiceConnection provideBriarServiceConnection() {
return new BriarServiceConnection(); return new BriarServiceConnection();
} }
} }

View File

@@ -92,6 +92,9 @@ public abstract class BaseActivity extends AppCompatActivity
.build(); .build();
injectActivity(activityComponent); injectActivity(activityComponent);
super.onCreate(state); super.onCreate(state);
if (LOG.isLoggable(INFO)) {
LOG.info("Creating " + getClass().getSimpleName());
}
// WARNING: When removing this or making it possible to turn it off, // WARNING: When removing this or making it possible to turn it off,
// we need a solution for the app lock feature. // we need a solution for the app lock feature.
@@ -127,8 +130,9 @@ public abstract class BaseActivity extends AppCompatActivity
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO)) {
LOG.info("Starting " + this.getClass().getSimpleName()); LOG.info("Starting " + getClass().getSimpleName());
}
for (ActivityLifecycleController alc : lifecycleControllers) { for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityStart(); alc.onActivityStart();
} }
@@ -144,11 +148,28 @@ public abstract class BaseActivity extends AppCompatActivity
return (ScreenFilterDialogFragment) f; return (ScreenFilterDialogFragment) f;
} }
@Override
protected void onResume() {
super.onResume();
if (LOG.isLoggable(INFO)) {
LOG.info("Resuming " + getClass().getSimpleName());
}
}
@Override
protected void onPause() {
super.onPause();
if (LOG.isLoggable(INFO)) {
LOG.info("Pausing " + getClass().getSimpleName());
}
}
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO)) {
LOG.info("Stopping " + this.getClass().getSimpleName()); LOG.info("Stopping " + getClass().getSimpleName());
}
for (ActivityLifecycleController alc : lifecycleControllers) { for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityStop(); alc.onActivityStop();
} }
@@ -203,6 +224,9 @@ public abstract class BaseActivity extends AppCompatActivity
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if (LOG.isLoggable(INFO)) {
LOG.info("Destroying " + getClass().getSimpleName());
}
destroyed = true; destroyed = true;
for (ActivityLifecycleController alc : lifecycleControllers) { for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityDestroy(); alc.onActivityDestroy();

View File

@@ -95,12 +95,14 @@ public abstract class BriarActivity extends BaseActivity {
// Also check that the activity isn't finishing already. // Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult(). // This is possible if we finished in onActivityResult().
// Launching another StartupActivity would cause a loop. // Launching another StartupActivity would cause a loop.
LOG.info("Not signed in, launching StartupActivity");
Intent i = new Intent(this, StartupActivity.class); Intent i = new Intent(this, StartupActivity.class);
startActivityForResult(i, REQUEST_PASSWORD); startActivityForResult(i, REQUEST_PASSWORD);
} else if (lockManager.isLocked() && !isFinishing()) { } else if (lockManager.isLocked() && !isFinishing()) {
// Also check that the activity isn't finishing already. // Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult(). // This is possible if we finished in onActivityResult().
// Launching another UnlockActivity would cause a loop. // Launching another UnlockActivity would cause a loop.
LOG.info("Locked, launching UnlockActivity");
Intent i = new Intent(this, UnlockActivity.class); Intent i = new Intent(this, UnlockActivity.class);
startActivityForResult(i, REQUEST_UNLOCK); startActivityForResult(i, REQUEST_UNLOCK);
} else if (SDK_INT >= 23) { } else if (SDK_INT >= 23) {

View File

@@ -61,7 +61,7 @@ import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListen
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static androidx.core.view.ViewCompat.getTransitionName; import static androidx.core.view.ViewCompat.getTransitionName;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -87,7 +87,12 @@ public class ContactListFragment extends BaseFragment implements EventListener,
private ContactListAdapter adapter; private ContactListAdapter adapter;
private BriarRecyclerView list; private BriarRecyclerView list;
private Snackbar snackbar; /**
* The Snackbar is non-null when shown and null otherwise.
* Use {@link #showSnackBar()} and {@link #dismissSnackBar()} to interact.
*/
@Nullable
private Snackbar snackbar = null;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject @Inject
@@ -163,13 +168,6 @@ public class ContactListFragment extends BaseFragment implements EventListener,
list.setEmptyText(getString(R.string.no_contacts)); list.setEmptyText(getString(R.string.no_contacts));
list.setEmptyAction(getString(R.string.no_contacts_action)); list.setEmptyAction(getString(R.string.no_contacts_action));
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, v ->
startActivity(new Intent(getContext(),
PendingContactListActivity.class)))
.make(contentView, R.string.pending_contact_requests_snackbar,
LENGTH_INDEFINITE);
return contentView; return contentView;
} }
@@ -203,9 +201,9 @@ public class ContactListFragment extends BaseFragment implements EventListener,
listener.runOnDbThread(() -> { listener.runOnDbThread(() -> {
try { try {
if (contactManager.getPendingContacts().isEmpty()) { if (contactManager.getPendingContacts().isEmpty()) {
runOnUiThreadUnlessDestroyed(() -> snackbar.dismiss()); runOnUiThreadUnlessDestroyed(this::dismissSnackBar);
} else { } else {
runOnUiThreadUnlessDestroyed(() -> snackbar.show()); runOnUiThreadUnlessDestroyed(this::showSnackBar);
} }
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
@@ -220,6 +218,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
adapter.clear(); adapter.clear();
list.showProgressBar(); list.showProgressBar();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
dismissSnackBar();
} }
private void loadContacts() { private void loadContacts() {
@@ -315,4 +314,27 @@ public class ContactListFragment extends BaseFragment implements EventListener,
} }
} }
@UiThread
private void showSnackBar() {
if (snackbar != null) return;
View v = requireNonNull(getView());
int stringRes = R.string.pending_contact_requests_snackbar;
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, view -> showPendingContactList())
.make(v, stringRes, LENGTH_INDEFINITE);
snackbar.show();
}
@UiThread
private void dismissSnackBar() {
if (snackbar == null) return;
snackbar.dismiss();
snackbar = null;
}
private void showPendingContactList() {
Intent i = new Intent(getContext(), PendingContactListActivity.class);
startActivity(i);
}
} }

View File

@@ -15,27 +15,33 @@ import android.widget.Toast;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.util.UiUtils;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard; import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
public class ChangePasswordActivity extends BriarActivity public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener { implements OnClickListener, OnEditorActionListener {
@Inject @Inject
protected ChangePasswordController passwordController; ViewModelProvider.Factory viewModelFactory;
private TextInputLayout currentPasswordEntryWrapper; private TextInputLayout currentPasswordEntryWrapper;
private TextInputLayout newPasswordEntryWrapper; private TextInputLayout newPasswordEntryWrapper;
@@ -47,11 +53,17 @@ public class ChangePasswordActivity extends BriarActivity
private Button changePasswordButton; private Button changePasswordButton;
private ProgressBar progress; private ProgressBar progress;
@VisibleForTesting
ChangePasswordViewModel viewModel;
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
setContentView(R.layout.activity_change_password); setContentView(R.layout.activity_change_password);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
currentPasswordEntryWrapper = currentPasswordEntryWrapper =
findViewById(R.id.current_password_entry_wrapper); findViewById(R.id.current_password_entry_wrapper);
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper); newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
@@ -102,13 +114,12 @@ public class ChangePasswordActivity extends BriarActivity
String firstPassword = newPassword.getText().toString(); String firstPassword = newPassword.getText().toString();
String secondPassword = newPasswordConfirmation.getText().toString(); String secondPassword = newPasswordConfirmation.getText().toString();
boolean passwordsMatch = firstPassword.equals(secondPassword); boolean passwordsMatch = firstPassword.equals(secondPassword);
float strength = float strength = viewModel.estimatePasswordStrength(firstPassword);
passwordController.estimatePasswordStrength(firstPassword);
strengthMeter.setStrength(strength); strengthMeter.setStrength(strength);
UiUtils.setError(newPasswordEntryWrapper, setError(newPasswordEntryWrapper,
getString(R.string.password_too_weak), getString(R.string.password_too_weak),
firstPassword.length() > 0 && strength < QUITE_WEAK); firstPassword.length() > 0 && strength < QUITE_WEAK);
UiUtils.setError(newPasswordConfirmationWrapper, setError(newPasswordConfirmationWrapper,
getString(R.string.passwords_do_not_match), getString(R.string.passwords_do_not_match),
secondPassword.length() > 0 && !passwordsMatch); secondPassword.length() > 0 && !passwordsMatch);
changePasswordButton.setEnabled( changePasswordButton.setEnabled(
@@ -127,32 +138,34 @@ public class ChangePasswordActivity extends BriarActivity
// Replace the button with a progress bar // Replace the button with a progress bar
changePasswordButton.setVisibility(INVISIBLE); changePasswordButton.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE); progress.setVisibility(VISIBLE);
passwordController.changePassword(currentPassword.getText().toString(),
newPassword.getText().toString(), String curPwd = currentPassword.getText().toString();
new UiResultHandler<Boolean>(this) { String newPwd = newPassword.getText().toString();
@Override viewModel.changePassword(curPwd, newPwd).observeEvent(this, result -> {
public void onResultUi(@NonNull Boolean result) { if (result == SUCCESS) {
if (result) { Toast.makeText(ChangePasswordActivity.this,
Toast.makeText(ChangePasswordActivity.this, R.string.password_changed,
R.string.password_changed, LENGTH_LONG).show();
Toast.LENGTH_LONG).show(); setResult(RESULT_OK);
setResult(RESULT_OK); supportFinishAfterTransition();
supportFinishAfterTransition(); } else {
} else { tryAgain(result);
tryAgain();
}
} }
}); }
);
} }
private void tryAgain() { private void tryAgain(DecryptionResult result) {
UiUtils.setError(currentPasswordEntryWrapper,
getString(R.string.try_again), true);
changePasswordButton.setVisibility(VISIBLE); changePasswordButton.setVisibility(VISIBLE);
progress.setVisibility(INVISIBLE); progress.setVisibility(INVISIBLE);
currentPassword.setText(""); if (result == KEY_STRENGTHENER_ERROR) {
createKeyStrengthenerErrorDialog(this).show();
// show the keyboard again } else {
showSoftKeyboard(currentPassword); setError(currentPasswordEntryWrapper,
getString(R.string.try_again), true);
currentPassword.setText("");
// show the keyboard again
showSoftKeyboard(currentPassword);
}
} }
} }

View File

@@ -1,43 +0,0 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultHandler;
import java.util.concurrent.Executor;
import javax.inject.Inject;
@NotNullByDefault
public class ChangePasswordControllerImpl implements ChangePasswordController {
protected final AccountManager accountManager;
protected final Executor ioExecutor;
private final PasswordStrengthEstimator strengthEstimator;
@Inject
ChangePasswordControllerImpl(AccountManager accountManager,
@IoExecutor Executor ioExecutor,
PasswordStrengthEstimator strengthEstimator) {
this.accountManager = accountManager;
this.ioExecutor = ioExecutor;
this.strengthEstimator = strengthEstimator;
}
@Override
public float estimatePasswordStrength(String password) {
return strengthEstimator.estimateStrength(password);
}
@Override
public void changePassword(String oldPassword, String newPassword,
ResultHandler<Boolean> resultHandler) {
ioExecutor.execute(() -> {
boolean changed =
accountManager.changePassword(oldPassword, newPassword);
resultHandler.onResult(changed);
});
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import androidx.lifecycle.ViewModel;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
@NotNullByDefault
public class ChangePasswordViewModel extends ViewModel {
private final AccountManager accountManager;
private final Executor ioExecutor;
private final PasswordStrengthEstimator strengthEstimator;
@Inject
ChangePasswordViewModel(AccountManager accountManager,
@IoExecutor Executor ioExecutor,
PasswordStrengthEstimator strengthEstimator) {
this.accountManager = accountManager;
this.ioExecutor = ioExecutor;
this.strengthEstimator = strengthEstimator;
}
float estimatePasswordStrength(String password) {
return strengthEstimator.estimateStrength(password);
}
LiveEvent<DecryptionResult> changePassword(String oldPassword,
String newPassword) {
MutableLiveEvent<DecryptionResult> result = new MutableLiveEvent<>();
ioExecutor.execute(() -> {
try {
accountManager.changePassword(oldPassword, newPassword);
result.postEvent(SUCCESS);
} catch (DecryptionException e) {
result.postEvent(e.getDecryptionResult());
}
});
return result;
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.login;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public abstract class LoginModule {
@Binds
@IntoMap
@ViewModelKey(StartupViewModel.class)
abstract ViewModel bindStartupViewModel(StartupViewModel viewModel);
@Binds
@IntoMap
@ViewModelKey(ChangePasswordViewModel.class)
abstract ViewModel bindChangePasswordViewModel(
ChangePasswordViewModel viewModel);
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.briar.android.login;
import android.content.Context;
import android.graphics.drawable.Drawable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.appcompat.app.AlertDialog;
import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.content.ContextCompat.getDrawable;
import static androidx.core.graphics.drawable.DrawableCompat.setTint;
import static java.util.Objects.requireNonNull;
@NotNullByDefault
class LoginUtils {
static AlertDialog createKeyStrengthenerErrorDialog(Context ctx) {
AlertDialog.Builder builder =
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error);
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
builder.setIcon(icon);
builder.setTitle(R.string.dialog_title_cannot_check_password);
builder.setMessage(R.string.dialog_message_cannot_check_password);
builder.setPositiveButton(R.string.ok, null);
return builder.create();
}
}

View File

@@ -12,6 +12,7 @@ import android.widget.ProgressBar;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -28,6 +29,9 @@ import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
import static org.briarproject.briar.android.util.UiUtils.enterPressed; import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.setError; import static org.briarproject.briar.android.util.UiUtils.setError;
@@ -58,12 +62,13 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_password, container, View v = inflater.inflate(R.layout.fragment_password, container,
false); false);
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
.get(StartupViewModel.class); .get(StartupViewModel.class);
viewModel.getPasswordValidated().observeEvent(this, valid -> {
if (!valid) onPasswordInvalid(); viewModel.getPasswordValidated().observeEvent(this, result -> {
if (result != SUCCESS) onPasswordInvalid(result);
}); });
signInButton = v.findViewById(R.id.btn_sign_in); signInButton = v.findViewById(R.id.btn_sign_in);
@@ -107,18 +112,20 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
viewModel.validatePassword(password.getText().toString()); viewModel.validatePassword(password.getText().toString());
} }
private void onPasswordInvalid() { private void onPasswordInvalid(DecryptionResult result) {
setError(input, getString(R.string.try_again), true);
signInButton.setVisibility(VISIBLE); signInButton.setVisibility(VISIBLE);
progress.setVisibility(INVISIBLE); progress.setVisibility(INVISIBLE);
password.setText(null); if (result == KEY_STRENGTHENER_ERROR) {
createKeyStrengthenerErrorDialog(requireContext()).show();
// show the keyboard again } else {
showSoftKeyboard(password); setError(input, getString(R.string.try_again), true);
password.setText(null);
// show the keyboard again
showSoftKeyboard(password);
}
} }
public void onForgottenPasswordClick() { private void onForgottenPasswordClick() {
// TODO Encapsulate the dialog in a re-usable fragment
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(), AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
R.style.BriarDialogTheme); R.style.BriarDialogTheme);
builder.setTitle(R.string.dialog_title_lost_password); builder.setTitle(R.string.dialog_title_lost_password);

View File

@@ -3,6 +3,8 @@ package org.briarproject.briar.android.login;
import android.app.Application; import android.app.Application;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
@@ -24,6 +26,7 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
@@ -46,7 +49,7 @@ public class StartupViewModel extends AndroidViewModel
@IoExecutor @IoExecutor
private final Executor ioExecutor; private final Executor ioExecutor;
private final MutableLiveEvent<Boolean> passwordValidated = private final MutableLiveEvent<DecryptionResult> passwordValidated =
new MutableLiveEvent<>(); new MutableLiveEvent<>();
private final MutableLiveEvent<Boolean> accountDeleted = private final MutableLiveEvent<Boolean> accountDeleted =
new MutableLiveEvent<>(); new MutableLiveEvent<>();
@@ -105,13 +108,17 @@ public class StartupViewModel extends AndroidViewModel
void validatePassword(String password) { void validatePassword(String password) {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
boolean signedIn = accountManager.signIn(password); try {
passwordValidated.postEvent(signedIn); accountManager.signIn(password);
if (signedIn) state.postValue(SIGNED_IN); passwordValidated.postEvent(SUCCESS);
state.postValue(SIGNED_IN);
} catch (DecryptionException e) {
passwordValidated.postEvent(e.getDecryptionResult());
}
}); });
} }
LiveEvent<Boolean> getPasswordValidated() { LiveEvent<DecryptionResult> getPasswordValidated() {
return passwordValidated; return passwordValidated;
} }

View File

@@ -381,7 +381,7 @@ public class UiUtils {
/** /**
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)}, * Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
* but without a {@link LifecycleOwner}. * but without a {@link LifecycleOwner}.
* * <p>
* Warning: Do NOT call from objects that have a lifecycle. * Warning: Do NOT call from objects that have a lifecycle.
*/ */
@UiThread @UiThread
@@ -401,5 +401,4 @@ public class UiUtils {
return ctx.getResources().getConfiguration().getLayoutDirection() == return ctx.getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL; LAYOUT_DIRECTION_RTL;
} }
} }

View File

@@ -4,7 +4,6 @@ import org.briarproject.briar.android.contact.add.remote.AddContactViewModel;
import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel; import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel;
import org.briarproject.briar.android.conversation.ConversationViewModel; import org.briarproject.briar.android.conversation.ConversationViewModel;
import org.briarproject.briar.android.conversation.ImageViewModel; import org.briarproject.briar.android.conversation.ImageViewModel;
import org.briarproject.briar.android.login.StartupViewModel;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -17,11 +16,6 @@ import dagger.multibindings.IntoMap;
@Module @Module
public abstract class ViewModelModule { public abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(StartupViewModel.class)
abstract ViewModel bindStartupViewModel(StartupViewModel startupViewModel);
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(ConversationViewModel.class) @ViewModelKey(ConversationViewModel.class)

View File

@@ -32,6 +32,8 @@
<!-- Login --> <!-- Login -->
<string name="enter_password">Password</string> <string name="enter_password">Password</string>
<string name="try_again">Wrong password, try again</string> <string name="try_again">Wrong password, try again</string>
<string name="dialog_title_cannot_check_password">Cannot Check Password</string>
<string name="dialog_message_cannot_check_password">Briar cannot check your password. Please try rebooting your device to solve this problem.</string>
<string name="sign_in_button">Sign In</string> <string name="sign_in_button">Sign In</string>
<string name="forgotten_password">I have forgotten my password</string> <string name="forgotten_password">I have forgotten my password</string>
<string name="dialog_title_lost_password">Lost Password</string> <string name="dialog_title_lost_password">Lost Password</string>

View File

@@ -5,28 +5,30 @@ import android.widget.EditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.TestBriarApplication; import org.briarproject.briar.android.TestBriarApplication;
import org.briarproject.briar.android.controller.handler.ResultHandler; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.NONE; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.NONE;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_STRONG; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.STRONG; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.STRONG;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.WEAK; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.WEAK;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -37,7 +39,7 @@ import static org.mockito.Mockito.when;
@Config(sdk = 21, application = TestBriarApplication.class) @Config(sdk = 21, application = TestBriarApplication.class)
public class ChangePasswordActivityTest { public class ChangePasswordActivityTest {
private TestChangePasswordActivity changePasswordActivity; private ChangePasswordActivity changePasswordActivity;
private TextInputLayout passwordConfirmationWrapper; private TextInputLayout passwordConfirmationWrapper;
private EditText currentPassword; private EditText currentPassword;
private EditText newPassword; private EditText newPassword;
@@ -46,15 +48,14 @@ public class ChangePasswordActivityTest {
private Button changePasswordButton; private Button changePasswordButton;
@Mock @Mock
private ChangePasswordController passwordController; private ChangePasswordViewModel viewModel;
@Captor
private ArgumentCaptor<ResultHandler<Boolean>> resultCaptor;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
changePasswordActivity = changePasswordActivity =
Robolectric.setupActivity(TestChangePasswordActivity.class); Robolectric.setupActivity(ChangePasswordActivity.class);
changePasswordActivity.viewModel = viewModel;
passwordConfirmationWrapper = changePasswordActivity passwordConfirmationWrapper = changePasswordActivity
.findViewById(R.id.new_password_confirm_wrapper); .findViewById(R.id.new_password_confirm_wrapper);
currentPassword = changePasswordActivity currentPassword = changePasswordActivity
@@ -81,7 +82,7 @@ public class ChangePasswordActivityTest {
// Password mismatch // Password mismatch
newPassword.setText("really.safe.password"); newPassword.setText("really.safe.password");
newPasswordConfirmation.setText("really.safe.pass"); newPasswordConfirmation.setText("really.safe.pass");
assertEquals(changePasswordButton.isEnabled(), false); assertFalse(changePasswordButton.isEnabled());
assertEquals(passwordConfirmationWrapper.getError(), assertEquals(passwordConfirmationWrapper.getError(),
changePasswordActivity changePasswordActivity
.getString(R.string.passwords_do_not_match)); .getString(R.string.passwords_do_not_match));
@@ -89,70 +90,59 @@ public class ChangePasswordActivityTest {
newPassword.setText("really.safe.pass"); newPassword.setText("really.safe.pass");
newPasswordConfirmation.setText("really.safe.pass"); newPasswordConfirmation.setText("really.safe.pass");
// Confirm that the password mismatch error message is not visible // Confirm that the password mismatch error message is not visible
Assert.assertNotEquals(passwordConfirmationWrapper.getError(), assertNotEquals(passwordConfirmationWrapper.getError(),
changePasswordActivity changePasswordActivity
.getString(R.string.passwords_do_not_match)); .getString(R.string.passwords_do_not_match));
// Nick has not been set, expect the button to be disabled // Nick has not been set, expect the button to be disabled
assertEquals(changePasswordButton.isEnabled(), false); assertFalse(changePasswordButton.isEnabled());
} }
@Test @Test
public void testChangePasswordUI() { public void testChangePasswordUI() {
changePasswordActivity.setPasswordController(passwordController);
// Mock strong password strength answer // Mock strong password strength answer
when(passwordController.estimatePasswordStrength(anyString())) when(viewModel.estimatePasswordStrength(anyString()))
.thenReturn(STRONG); .thenReturn(STRONG);
// Mock changing the password
MutableLiveEvent<DecryptionResult> result = new MutableLiveEvent<>();
when(viewModel.changePassword(anyString(), anyString()))
.thenReturn(result);
String curPass = "old.password"; String curPass = "old.password";
String safePass = "really.safe.password"; String safePass = "really.safe.password";
currentPassword.setText(curPass); currentPassword.setText(curPass);
newPassword.setText(safePass); newPassword.setText(safePass);
newPasswordConfirmation.setText(safePass); newPasswordConfirmation.setText(safePass);
// Confirm that the create account button is clickable // Confirm that the create account button is clickable
assertEquals(changePasswordButton.isEnabled(), true); assertTrue(changePasswordButton.isEnabled());
changePasswordButton.performClick(); changePasswordButton.performClick();
// Verify that the controller's method was called with the correct // Verify that the view model was called with the correct params
// params and get the callback verify(viewModel, times(1)).changePassword(eq(curPass), eq(safePass));
verify(passwordController, times(1)) // Return the result
.changePassword(eq(curPass), eq(safePass), result.postEvent(SUCCESS);
resultCaptor.capture()); assertTrue(changePasswordActivity.isFinishing());
// execute the callbacks
resultCaptor.getValue().onResult(true);
assertEquals(changePasswordActivity.isFinishing(), true);
} }
@Test @Test
public void testStrengthMeterUI() { public void testStrengthMeterUI() {
Assert.assertNotNull(changePasswordActivity); Assert.assertNotNull(changePasswordActivity);
// replace the password controller with our mocked copy
changePasswordActivity.setPasswordController(passwordController);
// Mock answers for UI testing only // Mock answers for UI testing only
when(passwordController.estimatePasswordStrength("strong")).thenReturn( when(viewModel.estimatePasswordStrength("strong")).thenReturn(STRONG);
STRONG); when(viewModel.estimatePasswordStrength("qstrong"))
when(passwordController.estimatePasswordStrength("qstrong")).thenReturn( .thenReturn(QUITE_STRONG);
QUITE_STRONG); when(viewModel.estimatePasswordStrength("qweak"))
when(passwordController.estimatePasswordStrength("qweak")).thenReturn( .thenReturn(QUITE_WEAK);
QUITE_WEAK); when(viewModel.estimatePasswordStrength("weak")).thenReturn(WEAK);
when(passwordController.estimatePasswordStrength("weak")).thenReturn( when(viewModel.estimatePasswordStrength("empty")).thenReturn(NONE);
WEAK);
when(passwordController.estimatePasswordStrength("empty")).thenReturn(
NONE);
// Test the meters progress and color for several values // Test the meters progress and color for several values
testStrengthMeter("strong", STRONG, StrengthMeter.GREEN); testStrengthMeter("strong", STRONG, StrengthMeter.GREEN);
Mockito.verify(passwordController, Mockito.times(1)) verify(viewModel, times(1)).estimatePasswordStrength(eq("strong"));
.estimatePasswordStrength(eq("strong"));
testStrengthMeter("qstrong", QUITE_STRONG, StrengthMeter.LIME); testStrengthMeter("qstrong", QUITE_STRONG, StrengthMeter.LIME);
Mockito.verify(passwordController, Mockito.times(1)) verify(viewModel, times(1)).estimatePasswordStrength(eq("qstrong"));
.estimatePasswordStrength(eq("qstrong"));
testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW); testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW);
Mockito.verify(passwordController, Mockito.times(1)) verify(viewModel, times(1)).estimatePasswordStrength(eq("qweak"));
.estimatePasswordStrength(eq("qweak"));
testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE); testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE);
Mockito.verify(passwordController, Mockito.times(1)) verify(viewModel, times(1)).estimatePasswordStrength(eq("weak"));
.estimatePasswordStrength(eq("weak"));
// Not sure this should be the correct behaviour on an empty input ? // Not sure this should be the correct behaviour on an empty input ?
testStrengthMeter("empty", NONE, StrengthMeter.RED); testStrengthMeter("empty", NONE, StrengthMeter.RED);
Mockito.verify(passwordController, Mockito.times(1)) verify(viewModel, times(1)).estimatePasswordStrength(eq("empty"));
.estimatePasswordStrength(eq("empty"));
} }
} }

View File

@@ -1,58 +0,0 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
public class ChangePasswordControllerImplTest extends BrambleMockTestCase {
private final AccountManager accountManager =
context.mock(AccountManager.class);
private final PasswordStrengthEstimator estimator =
context.mock(PasswordStrengthEstimator.class);
private final Executor ioExecutor = new ImmediateExecutor();
private final String oldPassword = getRandomString(10);
private final String newPassword = getRandomString(10);
@Test
public void testChangePasswordReturnsTrue() {
context.checking(new Expectations() {{
oneOf(accountManager).changePassword(oldPassword, newPassword);
will(returnValue(true));
}});
ChangePasswordControllerImpl p = new ChangePasswordControllerImpl(accountManager,
ioExecutor, estimator);
AtomicBoolean capturedResult = new AtomicBoolean(false);
p.changePassword(oldPassword, newPassword, capturedResult::set);
assertTrue(capturedResult.get());
}
@Test
public void testChangePasswordReturnsFalseIfOldPasswordIsWrong() {
context.checking(new Expectations() {{
oneOf(accountManager).changePassword(oldPassword, newPassword);
will(returnValue(false));
}});
ChangePasswordControllerImpl p = new ChangePasswordControllerImpl(accountManager,
ioExecutor, estimator);
AtomicBoolean capturedResult = new AtomicBoolean(true);
p.changePassword(oldPassword, newPassword, capturedResult::set);
assertFalse(capturedResult.get());
}
}

View File

@@ -1,14 +0,0 @@
package org.briarproject.briar.android.login;
/**
* This class exposes the PasswordController and offers the possibility to
* replace it.
*/
public class TestChangePasswordActivity extends ChangePasswordActivity {
public void setPasswordController(
ChangePasswordController passwordController) {
this.passwordController = passwordController;
}
}

View File

@@ -230,6 +230,8 @@ public class TestDataCreatorImpl implements TestDataCreator {
sb.append(getRandomLanAddress()); sb.append(getRandomLanAddress());
} }
lan.put(LanTcpConstants.PROP_IP_PORTS, sb.toString()); lan.put(LanTcpConstants.PROP_IP_PORTS, sb.toString());
String port = String.valueOf(getRandomPortNumber());
lan.put(LanTcpConstants.PROP_PORT, port);
props.put(LanTcpConstants.ID, lan); props.put(LanTcpConstants.ID, lan);
// Tor // Tor
@@ -266,18 +268,21 @@ public class TestDataCreatorImpl implements TestDataCreator {
sb.append("10."); sb.append("10.");
sb.append(random.nextInt(2)).append('.'); sb.append(random.nextInt(2)).append('.');
sb.append(random.nextInt(2)).append('.'); sb.append(random.nextInt(2)).append('.');
sb.append(random.nextInt(256)); sb.append(random.nextInt(255));
} else { } else {
sb.append("192.168."); sb.append("192.168.");
sb.append(random.nextInt(2)).append('.'); sb.append(random.nextInt(2)).append('.');
sb.append(random.nextInt(256)); sb.append(random.nextInt(255));
} }
// port // port
sb.append(":"); sb.append(':').append(getRandomPortNumber());
sb.append(1024 + random.nextInt(50000));
return sb.toString(); return sb.toString();
} }
private int getRandomPortNumber() {
return 32768 + random.nextInt(32768);
}
private String getRandomTorAddress() { private String getRandomTorAddress() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
// address // address

View File

@@ -67,7 +67,8 @@ Returns a JSON array of contacts:
"contactId": 1, "contactId": 1,
"alias" : "A local nickname", "alias" : "A local nickname",
"handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=", "handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=",
"verified": true "verified": true,
"lastChatActivity": 1557838312175
} }
``` ```

View File

@@ -4,6 +4,7 @@ import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.TermUi.echo import com.github.ajalt.clikt.output.TermUi.echo
import com.github.ajalt.clikt.output.TermUi.prompt import com.github.ajalt.clikt.output.TermUi.prompt
import org.briarproject.bramble.api.account.AccountManager import org.briarproject.bramble.api.account.AccountManager
import org.briarproject.bramble.api.crypto.DecryptionException
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
@@ -34,7 +35,9 @@ constructor(
} else { } else {
val password = prompt("Password", hideInput = true) val password = prompt("Password", hideInput = true)
?: throw UsageError("Could not get password. Is STDIN connected?") ?: throw UsageError("Could not get password. Is STDIN connected?")
if (!accountManager.signIn(password)) { try {
accountManager.signIn(password)
} catch (e : DecryptionException) {
echo("Error: Password invalid") echo("Error: Password invalid")
exitProcess(1) exitProcess(1)
} }

View File

@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.event.Event
import org.briarproject.bramble.api.event.EventListener import org.briarproject.bramble.api.event.EventListener
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.util.StringUtils.toUtf8 import org.briarproject.bramble.util.StringUtils.toUtf8
import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.headless.event.WebSocketController import org.briarproject.briar.headless.event.WebSocketController
import org.briarproject.briar.headless.getContactIdFromPathParam import org.briarproject.briar.headless.getContactIdFromPathParam
import org.briarproject.briar.headless.getFromJson import org.briarproject.briar.headless.getFromJson
@@ -38,6 +39,7 @@ internal class ContactControllerImpl
@Inject @Inject
constructor( constructor(
private val contactManager: ContactManager, private val contactManager: ContactManager,
private val conversationManager: ConversationManager,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val webSocket: WebSocketController private val webSocket: WebSocketController
) : ContactController, EventListener { ) : ContactController, EventListener {
@@ -61,7 +63,8 @@ constructor(
override fun list(ctx: Context): Context { override fun list(ctx: Context): Context {
val contacts = contactManager.contacts.map { contact -> val contacts = contactManager.contacts.map { contact ->
contact.output() val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime
contact.output(latestMsgTime)
} }
return ctx.json(contacts) return ctx.json(contacts)
} }

View File

@@ -3,12 +3,14 @@ package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.event.ContactAddedEvent import org.briarproject.bramble.api.contact.event.ContactAddedEvent
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.headless.json.JsonDict import org.briarproject.briar.headless.json.JsonDict
internal fun Contact.output() = JsonDict( internal fun Contact.output(latestMsgTime: Long) = JsonDict(
"contactId" to id.int, "contactId" to id.int,
"author" to author.output(), "author" to author.output(),
"verified" to isVerified "verified" to isVerified,
"lastChatActivity" to latestMsgTime
).apply { ).apply {
alias?.let { put("alias", it) } alias?.let { put("alias", it) }
handshakePublicKey?.let { put("handshakePublicKey", it.encoded) } handshakePublicKey?.let { put("handshakePublicKey", it.encoded) }

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.headless package org.briarproject.briar.headless
import org.briarproject.bramble.api.account.AccountManager import org.briarproject.bramble.api.account.AccountManager
import org.briarproject.bramble.api.crypto.DecryptionException
import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.bramble.api.lifecycle.LifecycleManager
import javax.annotation.concurrent.Immutable import javax.annotation.concurrent.Immutable
import javax.inject.Inject import javax.inject.Inject
@@ -23,7 +24,9 @@ constructor(
accountManager.deleteAccount() accountManager.deleteAccount()
} }
accountManager.createAccount(user, pass) accountManager.createAccount(user, pass)
if (!accountManager.signIn(pass)) { try {
accountManager.signIn(pass)
} catch (e: DecryptionException) {
throw AssertionError("Password invalid") throw AssertionError("Password invalid")
} }
val dbKey = accountManager.databaseKey ?: throw AssertionError() val dbKey = accountManager.databaseKey ?: throw AssertionError()

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.sync.Message
import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.test.TestUtils.* import org.briarproject.bramble.test.TestUtils.*
import org.briarproject.bramble.util.StringUtils.getRandomString import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.headless.event.WebSocketController import org.briarproject.briar.headless.event.WebSocketController
import org.skyscreamer.jsonassert.JSONAssert.assertEquals import org.skyscreamer.jsonassert.JSONAssert.assertEquals
import org.skyscreamer.jsonassert.JSONCompareMode.STRICT import org.skyscreamer.jsonassert.JSONCompareMode.STRICT
@@ -23,6 +24,7 @@ import javax.servlet.http.HttpServletResponse
abstract class ControllerTest { abstract class ControllerTest {
protected val contactManager = mockk<ContactManager>() protected val contactManager = mockk<ContactManager>()
protected val conversationManager = mockk<ConversationManager>()
protected val identityManager = mockk<IdentityManager>() protected val identityManager = mockk<IdentityManager>()
protected val clock = mockk<Clock>() protected val clock = mockk<Clock>()
protected val ctx = mockk<Context>() protected val ctx = mockk<Context>()

View File

@@ -34,8 +34,12 @@ internal class ContactControllerTest : ControllerTest() {
private val pendingContact = getPendingContact() private val pendingContact = getPendingContact()
private val controller = private val controller = ContactControllerImpl(
ContactControllerImpl(contactManager, objectMapper, webSocketController) contactManager,
conversationManager,
objectMapper,
webSocketController
)
@Test @Test
fun testEmptyContactList() { fun testEmptyContactList() {
@@ -47,7 +51,8 @@ internal class ContactControllerTest : ControllerTest() {
@Test @Test
fun testList() { fun testList() {
every { contactManager.contacts } returns listOf(contact) every { contactManager.contacts } returns listOf(contact)
every { ctx.json(listOf(contact.output())) } returns ctx every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp
every { ctx.json(listOf(contact.output(timestamp))) } returns ctx
controller.list(ctx) controller.list(ctx)
} }
@@ -271,10 +276,11 @@ internal class ContactControllerTest : ControllerTest() {
"author": ${toJson(author.output())}, "author": ${toJson(author.output())},
"alias" : "${contact.alias}", "alias" : "${contact.alias}",
"handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)}, "handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)},
"verified": ${contact.isVerified} "verified": ${contact.isVerified},
"lastChatActivity": $timestamp
} }
""" """
assertJsonEquals(json, contact.output()) assertJsonEquals(json, contact.output(timestamp))
} }
@Test @Test

View File

@@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test
internal class MessagingControllerImplTest : ControllerTest() { internal class MessagingControllerImplTest : ControllerTest() {
private val messagingManager = mockk<MessagingManager>() private val messagingManager = mockk<MessagingManager>()
private val conversationManager = mockk<ConversationManager>()
private val privateMessageFactory = mockk<PrivateMessageFactory>() private val privateMessageFactory = mockk<PrivateMessageFactory>()
private val dbExecutor = ImmediateExecutor() private val dbExecutor = ImmediateExecutor()