Compare commits

..

111 Commits

Author SHA1 Message Date
Torsten Grote
1aec948f18 Only show Introduction Accept Information if success is still possible 2018-04-28 11:45:10 -03:00
Torsten Grote
ebf73716bb Merge branch '474-manual-decline' into 'master'
Don't automatically respond to declined introductions

See merge request akwizgran/briar!777
2018-04-28 13:46:40 +00:00
akwizgran
6e42377b74 Don't automatically respond to declined introduction. 2018-04-28 00:11:45 +01:00
akwizgran
e8f33c0e6e Merge branch 'introduction-ui-messages' into 'master'
Fix introduction response messages in UI and some minor fixes

Closes #923

See merge request akwizgran/briar!776
2018-04-27 21:42:10 +00:00
Torsten Grote
5f6af4e40f Fix introduction response messages in UI and some minor fixes 2018-04-27 16:22:10 -03:00
Torsten Grote
55a329a879 Merge branch '474-automatic-decline' into 'master'
Send automatic decline when other introducee declines

See merge request akwizgran/briar!775
2018-04-27 17:58:45 +00:00
akwizgran
23f0864d8b Don't track invisible decline message. 2018-04-27 18:35:07 +01:00
akwizgran
c0dfe3e85a Sent automatic decline when other introducee declines. 2018-04-27 17:33:24 +01:00
Torsten Grote
31b69577e8 Merge branch '474-introduction-client' into 'master'
New Introduction Protocol

Closes #308, #377, #474, and #613

See merge request akwizgran/briar!758
2018-04-27 14:43:01 +00:00
Torsten Grote
99dba69c87 Only add transport properties and keys when the contact was added
This will be changed once we have a way to reset state for peers
that were contacts already at some point in the past.
One contact might have deleted the other, but not vice versa.
So they have mismatching state that needs to be reset.

See #2 for more information.
2018-04-27 11:30:18 -03:00
Torsten Grote
44f5a9db1e Address last review comments 2018-04-27 11:04:08 -03:00
Torsten Grote
80a9689316 Address second round of review comments 2018-04-26 20:39:17 -03:00
Torsten Grote
337f7e7b8f Unify introduction response methods and handle ProtocolStateException
It is possible that a remote DECLINE message arrives short before the
user responds to the introduction.
This will cause a ProtocolStateException which (for now) is just caught
and a generic (existing) error message will be shown.
2018-04-26 18:18:31 -03:00
Torsten Grote
f8f98ed95d Properly handle DECLINE messages in START state
Previously, DECLINE messages let directly to the START state
for introducer and introducees.
So incoming ACCEPT and DECLINE messages needed to be ignored in START state
introducing undefined behavior into the protocol.

This is fixed with this commit by adding two additional states
to the introducer state machine as well as making use of the existing
LOCAL_DECLINED state for the introducees.
2018-04-26 18:00:57 -03:00
Torsten Grote
bd5504de26 Add a MAC to the ACTIVATE message to prevent the introducer to fake them
A fake ACTIVATE message would cause us to activate the transport keys
before the contact has received our auth message,
which would compromise forward secrecy.
2018-04-26 16:56:38 -03:00
Torsten Grote
0e04044ebb Ensure that incoming messages are expected in the current state
Previously, the introducer would process and forward invalid messages by
the introducees. This commit adds the necessary checks and tests.
2018-04-26 11:18:04 -03:00
Torsten Grote
0a5d408686 Add a test for when one introducee had deleted the other one 2018-04-25 14:42:17 -03:00
Torsten Grote
f94db28035 Handle and test introductions to existing contacts 2018-04-25 13:30:51 -03:00
Torsten Grote
b291fcd2cd Only allow new introductions in START state
When the user attempts an introduction, instead of the introduction
message input field, an explanatory text will be shown and the
introduction can not be made until the last one has been finished.
2018-04-25 12:05:15 -03:00
Torsten Grote
94a6137a42 Also validate encoded message in MessageEncoder test 2018-04-25 10:52:32 -03:00
Torsten Grote
72e9a9d807 Address first round of review comments for new IntroductionClient 2018-04-25 10:43:56 -03:00
Torsten Grote
a9b678df32 Remove broken and deprecated MessageQueue as it is not needed anymore
Closes #308
2018-04-25 10:14:04 -03:00
Torsten Grote
f81ef30b47 Replace old introduction client with new one 2018-04-25 10:14:01 -03:00
Torsten Grote
1bc29fec06 IntroductionManager and Protocol Engines 2018-04-25 10:13:41 -03:00
Torsten Grote
61b216f572 Copy over Introduction API messages and events from old client 2018-04-25 10:13:41 -03:00
Torsten Grote
d57102ed90 IntroductionCrypto: Create dedicated class to handle introduction related crypto 2018-04-25 10:13:40 -03:00
Torsten Grote
e1fae7ad95 Implement SessionEncoder and SessionParser 2018-04-25 10:13:40 -03:00
Torsten Grote
672a52b2e5 Implement MessageEncoder and MessageParser 2018-04-25 10:13:39 -03:00
Torsten Grote
155c6a5613 Messages and Validator for new Introduction Client 2018-04-25 10:13:39 -03:00
Torsten Grote
218b2f7ff9 Fix activating transport keys in JdbcDatabase 2018-04-25 10:13:38 -03:00
Torsten Grote
f78f065204 Merge branch 'constant-time-mac-verification' into 'master'
Add constant-time method for verifying MACs

See merge request akwizgran/briar!773
2018-04-25 12:08:49 +00:00
akwizgran
0217c205a1 Add constant-time method for verifying MACs. 2018-04-25 12:23:46 +01:00
akwizgran
615f527270 Renamed method that now runs on IoExecutor. 2018-04-24 17:43:28 +01:00
akwizgran
b9cf1da861 Merge branch 'test_data_config' into 'master'
Make test data creation configurable.

Closes #1149

See merge request akwizgran/briar!771
2018-04-24 16:35:15 +00:00
goapunk
69c34adae3 Remove createTestData() 2018-04-24 16:57:02 +02:00
goapunk
fe213d46e3 Address review comments 2018-04-24 16:05:46 +02:00
goapunk
ac1bfcae60 Make test data creation configurable. 2018-04-24 12:29:20 +02:00
akwizgran
b30c2a8033 Merge branch 'fix_bt_test_data' into 'master'
Create a valid BT MAC and UUID when creating testdata

See merge request akwizgran/briar!769
2018-04-24 09:26:42 +00:00
goapunk
575847cb36 Create a valid BT MAC and UUID when creating testdata 2018-04-23 21:05:14 +02:00
akwizgran
951605151f Merge branch 'transport-properties' into 'master'
Helper methods for dealing with TransportProperties

See merge request akwizgran/briar!768
2018-04-23 09:05:14 +00:00
Torsten Grote
05735e7a48 Add methods for dealing with TransportProperties to ClientHelper 2018-04-21 18:23:34 -03:00
Torsten Grote
f835e82653 Merge branch 'download-briar-button' into 'master'
Add download button to ExpiredActivity

See merge request akwizgran/briar!766
2018-04-19 17:11:49 +00:00
akwizgran
d074e4a3d6 Add download button to ExpiredActivity. 2018-04-19 17:39:07 +01:00
akwizgran
87a92c9ab6 Merge branch 'parse-transport-properties' into 'master'
Helper method for parsing transport properties

See merge request akwizgran/briar!764
2018-04-19 16:01:16 +00:00
akwizgran
89cc769dea Don't accept empty keys/values in transport properties. 2018-04-19 16:48:59 +01:00
akwizgran
fcdc6ebafd Helper methods for parsing transport properties. 2018-04-19 16:20:32 +01:00
akwizgran
215d236c2c Merge branch 'forward-compatible-message-ids' into 'master'
Generate message and group IDs in a forward-compatible way

See merge request akwizgran/briar!763
2018-04-19 14:55:39 +00:00
Torsten Grote
e1b8b271e3 Merge branch 'offline-build' into 'master'
Don't download Tor binaries if they already exist

See merge request akwizgran/briar!760
2018-04-19 13:08:21 +00:00
akwizgran
9379990480 Use block label for root hash of single-block messages. 2018-04-19 13:13:31 +01:00
akwizgran
c7718db419 Skip second verification if first succeeds. 2018-04-18 17:01:02 +01:00
akwizgran
9196169561 Generate message and group IDs in a forward-compatible way. 2018-04-18 16:34:02 +01:00
akwizgran
ff9971b728 If verification fails, delete, download and re-verify. 2018-04-18 11:03:39 +01:00
Torsten Grote
8decc73f4d Merge branch 'max-client-id-length' into 'master'
Set max length for client IDs

See merge request akwizgran/briar!762
2018-04-17 19:01:22 +00:00
akwizgran
d23fc2cbda Use TestUtils to create groups. 2018-04-17 17:57:06 +01:00
akwizgran
58d1707467 Set max length for client IDs. 2018-04-17 17:57:00 +01:00
akwizgran
b08b2c691c Merge branch 'merge-contact-hooks' into 'master'
Merge add/remove contact hooks

See merge request akwizgran/briar!761
2018-04-17 16:53:22 +00:00
akwizgran
284e3a2e86 Merge add/remove contact hooks. 2018-04-17 16:20:16 +01:00
akwizgran
0823934e28 Don't download Tor binaries if they already exist. 2018-04-17 15:07:52 +01:00
akwizgran
95b9b3a3c6 Merge branch 'multiple-transport-keys' into 'master'
Support multiple sets of transport keys per contact

See merge request akwizgran/briar!745
2018-04-17 14:02:45 +00:00
Torsten Grote
ede390b897 Merge branch '1180-tor-plugin-status' into 'master'
Don't reset Tor connectivity state unless we lose connectivity

Closes #1180

See merge request akwizgran/briar!756
2018-03-30 15:49:20 +00:00
akwizgran
5b790130d4 Don't reset circuit built flag unless network is disabled. 2018-03-30 16:33:03 +01:00
akwizgran
ff44edf714 Merge branch '1171-wifi-access-point' into 'master'
Enable LAN plugin when providing a wifi access point

Closes #1171

See merge request akwizgran/briar!752
2018-03-29 15:17:46 +00:00
Torsten Grote
60dffd0998 Update translations, add Romanian 2018-03-29 10:38:01 -03:00
akwizgran
46dae59444 Delay handling of AP enabled event. 2018-03-29 14:34:58 +01:00
Torsten Grote
e385d58148 Merge branch '1190-shutdown-from-background' into 'master'
Shut down cleanly when phone is shutting down or memory is low

Closes #1190

See merge request akwizgran/briar!742
2018-03-29 13:29:42 +00:00
akwizgran
b20626935e AP state change event races with address appearing. 2018-03-29 12:26:42 +01:00
akwizgran
8e9fc3b338 Enable LAN plugin to use wifi AP interface. 2018-03-29 12:26:42 +01:00
akwizgran
c3a70fe58d Serialise concurrent calls to updateConnectionStatus(). 2018-03-29 12:26:42 +01:00
akwizgran
ddfaddccdc Serialise concurrent calls to bind(). 2018-03-29 12:26:41 +01:00
akwizgran
4a892acdd5 Merge branch '845-wifi-without-internet' into 'master'
Looks like I added the last commit after the MR was merged.
2018-03-29 12:25:02 +01:00
akwizgran
bdb518ff09 Use wifi network's socket factory on API 21+. 2018-03-29 12:23:42 +01:00
akwizgran
8ebced9481 Show notification for low memory shutdown. 2018-03-29 11:45:03 +01:00
akwizgran
b81058d6da Activate outgoing keys when incoming tag is recognised. 2018-03-28 12:39:03 +01:00
akwizgran
f7c2f86499 Add a method for checking whether we can send streams. 2018-03-28 12:39:03 +01:00
akwizgran
798b871cc9 Use key set ID to increment stream counter. 2018-03-28 12:39:02 +01:00
akwizgran
6787d29f11 Add a flag to indicate whether outgoing keys are active. 2018-03-28 12:39:02 +01:00
akwizgran
57e6f2ea9c Unit tests for removing unbound keys. 2018-03-28 12:39:02 +01:00
akwizgran
0a802bbe0b Add a method for removing unbound transport keys. 2018-03-28 12:39:02 +01:00
akwizgran
17fe358fd9 Add a method for binding transport keys to a contact. 2018-03-28 12:39:01 +01:00
akwizgran
5bd2092a03 Return key set IDs when adding unbound keys. 2018-03-28 12:39:01 +01:00
akwizgran
cb8f89db53 Add method for adding a contact without transport keys. 2018-03-28 12:39:01 +01:00
akwizgran
bb2f94d5eb Add methods for adding unbound keys. 2018-03-28 12:39:01 +01:00
akwizgran
78f2d48bc4 Support multiple sets of transport keys per contact. 2018-03-28 12:38:59 +01:00
Torsten Grote
309c7a4668 Merge branch '965-empty-state-messages' into 'master'
Shorten and clean up various strings, remove empty forum warning bubble

See merge request akwizgran/briar!741
2018-03-26 16:59:12 +00:00
Torsten Grote
750f2b1b75 Merge branch 'hide-ui-during-shutdown' into 'master'
Hide UI during shutdown

See merge request akwizgran/briar!737
2018-03-26 16:53:16 +00:00
Torsten Grote
e7b2fe1906 Merge branch '545-message-dependencies' into 'master'
Add denormalised columns to messageDependencies table

See merge request akwizgran/briar!733
2018-03-26 15:58:01 +00:00
akwizgran
bfd22cfced Merge branch '346-full-screen-qr-code' into 'master'
Add fullscreen button to QR code view

Closes #346

See merge request akwizgran/briar!734
2018-03-26 15:30:30 +00:00
akwizgran
ea0223ef1e Merge branch '1159-android-8-notification-settings' into 'master'
Show different notification settings for Android O

Closes #1159

See merge request akwizgran/briar!727
2018-03-26 15:24:28 +00:00
Torsten Grote
38b739442c Merge branch '845-wifi-without-internet' into 'master'
Use WifiManager to get wifi network information

Closes #845

See merge request akwizgran/briar!743
2018-03-26 14:54:03 +00:00
Torsten Grote
25f9ab7c33 Merge branch '1184-rejected-execution-exception' into 'master'
Discard tasks submitted to ScheduledExecutorService during shutdown

Closes #1184

See merge request akwizgran/briar!739
2018-03-26 14:27:42 +00:00
akwizgran
e0a1fa559d Use WifiManager to get wifi network information.
This ensures we bind to the wifi interface even if it doesn't have internet access and there's another interface with internet access (e.g. mobile data).
2018-03-26 13:58:10 +01:00
akwizgran
196cf15ef2 Shut down cleanly when device shuts down. 2018-03-21 14:42:30 +00:00
akwizgran
6ff0f317a5 Shut down cleanly when memory is low. 2018-03-21 14:31:12 +00:00
Torsten Grote
5a3f47d72c Merge branch '965-forum-empty-state' into 'master'
Remove mention of pen icon from forum empty state message

Closes #965

See merge request akwizgran/briar!740
2018-03-21 11:45:30 +00:00
akwizgran
7e784c6be1 Remove empty forum warning bubble. 2018-03-21 10:30:49 +00:00
akwizgran
3ee212f3ab Consistent text for blogs and forums. 2018-03-21 10:28:10 +00:00
akwizgran
ee942790d3 Shorter empty state messages. 2018-03-21 10:27:09 +00:00
akwizgran
2d740675c7 Consistent explanation of account deletion options. 2018-03-21 10:24:17 +00:00
akwizgran
e4f3960ce0 Remove mention of pen icon from forum empty state message. 2018-03-21 10:18:18 +00:00
akwizgran
fef916991b Discard tasks submitted during shutdown. 2018-03-20 17:41:30 +00:00
akwizgran
3fa38d3b28 Finish if back button is pressed in SignOutFragment. 2018-03-20 16:50:40 +00:00
akwizgran
48c41f77c7 Use database icon for SignOutFragment. 2018-03-20 16:50:40 +00:00
akwizgran
c3bf82c5b2 Close NavDrawerActivity immediately when signing out. 2018-03-20 16:50:40 +00:00
akwizgran
74fe36c46e Use selectable item background to get touch effect. 2018-03-20 15:30:46 +00:00
akwizgran
0d5d1f4cb2 Adjust layout weights when resizing QR code view. 2018-03-20 15:22:58 +00:00
akwizgran
2c4d5680a6 Add fullscreen button to QR code view. 2018-03-20 11:14:27 +00:00
akwizgran
9e2e0585c5 Index dependencies by dependency ID. 2018-03-14 16:08:01 +00:00
akwizgran
2367e6c481 Add denormalised columns to messageDependencies table. 2018-03-14 15:33:00 +00:00
Torsten Grote
ff8b38f7e8 Use a different notification preference summary for Android 8 2018-03-14 10:01:38 -03:00
Torsten Grote
f609ad1a92 Show different notification settings for Android O
This also makes the defaults consistent with Android versions below O.
2018-03-14 09:41:09 -03:00
akwizgran
a0c88da1ac Report UNKNOWN state for cross-group dependencies.
This causes the validator to treat the dependent message in the same way regardless of whether there is a subscription to the dependency's group.
2018-03-14 11:25:24 +00:00
223 changed files with 9746 additions and 7515 deletions

View File

@@ -1,6 +1,8 @@
import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify
import java.security.NoSuchAlgorithmException
apply plugin: 'com.android.library'
apply plugin: 'witness'
apply plugin: 'de.undercouch.download'
@@ -67,30 +69,68 @@ def torBinaries = [
"geoip" : '8239b98374493529a29096e45fc5877d4d6fdad0146ad8380b291f90d61484ea'
]
def downloadBinary(name) {
return tasks.create("downloadBinary${name}", Download) {
def verifyOrDeleteBinary(name, chksum, alreadyVerified) {
return tasks.create("verifyOrDeleteBinary${name}", VerifyOrDelete) {
src "${torBinaryDir}/${name}.zip"
algorithm 'SHA-256'
checksum chksum
result alreadyVerified
onlyIf {
src.exists()
}
}
}
def downloadBinary(name, chksum, alreadyVerified) {
return tasks.create([
name: "downloadBinary${name}",
type: Download,
dependsOn: verifyOrDeleteBinary(name, chksum, alreadyVerified)]) {
src "${torDownloadUrl}${name}.zip"
.replace('tor_', "tor-${torVersion}-")
.replace('geoip', "geoip-${geoipVersion}")
.replaceAll('_', '-')
dest "${torBinaryDir}/${name}.zip"
onlyIfNewer true
onlyIf {
!dest.exists()
}
}
}
def verifyBinary(name, chksum) {
boolean[] alreadyVerified = [false]
return tasks.create([
name : "verifyBinary${name}",
type : Verify,
dependsOn: downloadBinary(name)]) {
dependsOn: downloadBinary(name, chksum, alreadyVerified)]) {
src "${torBinaryDir}/${name}.zip"
algorithm 'SHA-256'
checksum chksum
onlyIf {
!alreadyVerified[0]
}
}
}
project.afterEvaluate {
torBinaries.every { key, value ->
preBuild.dependsOn.add(verifyBinary(key, value))
torBinaries.every { name, checksum ->
preBuild.dependsOn.add(verifyBinary(name, checksum))
}
}
class VerifyOrDelete extends Verify {
boolean[] result
@TaskAction
@Override
void verify() throws IOException, NoSuchAlgorithmException {
try {
super.verify()
result[0] = true
} catch (Exception e) {
println "${src} failed verification - deleting"
src.delete()
}
}
}

View File

@@ -48,7 +48,7 @@ public class AndroidPluginModule {
appContext, locationUtils, reporter, eventBus,
torSocketFactory, backoffFactory);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
backoffFactory, appContext);
scheduler, backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex =
Arrays.asList(bluetooth, tor, lan);
@NotNullByDefault

View File

@@ -5,43 +5,84 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.SocketFactory;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.util.AndroidUtils.logNetworkState;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.SECONDS;
@NotNullByDefault
class AndroidLanTcpPlugin extends LanTcpPlugin {
private static final String WIFI_AP_STATE_ACTION =
// See android.net.wifi.WifiManager
private static final String WIFI_AP_STATE_CHANGED_ACTION =
"android.net.wifi.WIFI_AP_STATE_CHANGED";
private static final int WIFI_AP_STATE_ENABLED = 13;
private static final byte[] WIFI_AP_ADDRESS_BYTES =
{(byte) 192, (byte) 168, 43, 1};
private static final InetAddress WIFI_AP_ADDRESS;
private static final Logger LOG =
Logger.getLogger(AndroidLanTcpPlugin.class.getName());
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 ScheduledExecutorService scheduler;
private final Context appContext;
private final ConnectivityManager connectivityManager;
@Nullable
private final WifiManager wifiManager;
@Nullable
private volatile BroadcastReceiver networkStateReceiver = null;
private volatile SocketFactory socketFactory;
AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff,
Context appContext, DuplexPluginCallback callback, int maxLatency,
int maxIdleTime) {
AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
Backoff backoff, Context appContext, DuplexPluginCallback callback,
int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
this.scheduler = scheduler;
this.appContext = appContext;
ConnectivityManager connectivityManager = (ConnectivityManager)
appContext.getSystemService(CONNECTIVITY_SERVICE);
if (connectivityManager == null) throw new AssertionError();
this.connectivityManager = connectivityManager;
wifiManager = (WifiManager) appContext.getApplicationContext()
.getSystemService(WIFI_SERVICE);
socketFactory = SocketFactory.getDefault();
}
@Override
@@ -52,9 +93,8 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
networkStateReceiver = new NetworkStateReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(CONNECTIVITY_ACTION);
filter.addAction(WIFI_AP_STATE_ACTION);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
appContext.registerReceiver(networkStateReceiver, filter);
if (LOG.isLoggable(INFO)) logNetworkState(appContext, LOG);
}
@Override
@@ -65,38 +105,92 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
tryToClose(socket);
}
@Override
protected Socket createSocket() throws IOException {
return socketFactory.createSocket();
}
@Override
protected Collection<InetAddress> getLocalIpAddresses() {
// If the device doesn't have wifi, don't open any sockets
if (wifiManager == null) return emptyList();
// If we're connected to a wifi network, use that network
WifiInfo info = wifiManager.getConnectionInfo();
if (info != null && info.getIpAddress() != 0)
return singletonList(intToInetAddress(info.getIpAddress()));
// If we're running an access point, return its address
if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
return singletonList(WIFI_AP_ADDRESS);
// No suitable addresses
return emptyList();
}
private InetAddress intToInetAddress(int ip) {
byte[] ipBytes = new byte[4];
ipBytes[0] = (byte) (ip & 0xFF);
ipBytes[1] = (byte) ((ip >> 8) & 0xFF);
ipBytes[2] = (byte) ((ip >> 16) & 0xFF);
ipBytes[3] = (byte) ((ip >> 24) & 0xFF);
try {
return InetAddress.getByAddress(ipBytes);
} catch (UnknownHostException e) {
// Should only be thrown if address has illegal length
throw new AssertionError(e);
}
}
// On API 21 and later, a socket that is not created with the wifi
// network's socket factory may try to connect via another network
private SocketFactory getSocketFactory() {
if (SDK_INT < 21) return SocketFactory.getDefault();
for (Network net : connectivityManager.getAllNetworks()) {
NetworkInfo info = connectivityManager.getNetworkInfo(net);
if (info != null && info.getType() == TYPE_WIFI)
return net.getSocketFactory();
}
LOG.warning("Could not find suitable socket factory");
return SocketFactory.getDefault();
}
private class NetworkStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent i) {
if (!running) return;
if (LOG.isLoggable(INFO)) {
if (CONNECTIVITY_ACTION.equals(i.getAction())) {
LOG.info("Connectivity change");
Bundle extras = i.getExtras();
if (extras != null) {
LOG.info("Extras:");
for (String key : extras.keySet())
LOG.info("\t" + key + ": " + extras.get(key));
}
} else if (WIFI_AP_STATE_ACTION.equals(i.getAction())) {
int state = i.getIntExtra(EXTRA_WIFI_STATE, 0);
if (state == 13) LOG.info("Wifi AP enabled");
else LOG.info("Wifi AP state " + state);
}
logNetworkState(appContext, LOG);
}
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
if (net != null && net.getType() == TYPE_WIFI
&& net.isConnected()) {
LOG.info("Connected to Wi-Fi");
if (socket == null || socket.isClosed()) bind();
if (isApEnabledEvent(i)) {
// The state change may be broadcast before the AP address is
// visible, so delay handling the event
scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
} else {
LOG.info("Not connected to Wi-Fi");
tryToClose(socket);
handleConnectivityChange();
}
}
private void handleConnectivityChange() {
if (!running) return;
Collection<InetAddress> addrs = getLocalIpAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)) {
LOG.info("Providing wifi hotspot");
// There's no corresponding Network object and thus no way
// to get a suitable socket factory, so we won't be able to
// make outgoing connections on API 21+ if another network
// has internet access
socketFactory = SocketFactory.getDefault();
if (socket == null || socket.isClosed()) bind();
} else if (addrs.isEmpty()) {
LOG.info("Not connected to wifi");
socketFactory = SocketFactory.getDefault();
tryToClose(socket);
} else {
LOG.info("Connected to wifi");
socketFactory = getSocketFactory();
if (socket == null || socket.isClosed()) bind();
}
}
private boolean isApEnabledEvent(Intent i) {
return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
}
}
}

View File

@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.concurrent.Immutable;
@@ -27,12 +28,15 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final BackoffFactory backoffFactory;
private final Context appContext;
public AndroidLanTcpPluginFactory(Executor ioExecutor,
BackoffFactory backoffFactory, Context appContext) {
ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
Context appContext) {
this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.backoffFactory = backoffFactory;
this.appContext = appContext;
}
@@ -51,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new AndroidLanTcpPlugin(ioExecutor, backoff, appContext,
callback, MAX_LATENCY, MAX_IDLE_TIME);
return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
appContext, callback, MAX_LATENCY, MAX_IDLE_TIME);
}
}

View File

@@ -16,6 +16,7 @@ import android.os.PowerManager;
import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
@@ -63,8 +64,6 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
@@ -111,7 +110,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private static final Logger LOG =
Logger.getLogger(TorPlugin.class.getName());
private final Executor ioExecutor;
private final Executor ioExecutor, connectionStatusExecutor;
private final ScheduledExecutorService scheduler;
private final Context appContext;
private final LocationUtils locationUtils;
@@ -125,7 +124,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final File torDirectory, torFile, geoIpFile, configFile;
private final File doneFile, cookieFile;
private final PowerManager.WakeLock wakeLock;
private final Lock connectionStatusLock;
private final AtomicReference<Future<?>> connectivityCheck =
new AtomicReference<>();
private final AtomicBoolean used = new AtomicBoolean(false);
@@ -167,7 +165,9 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// This tag will prevent Huawei's powermanager from killing us.
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
wakeLock.setReferenceCounted(false);
connectionStatusLock = new ReentrantLock();
// Don't execute more than one connection status check at a time
connectionStatusExecutor = new PoliteExecutor("TorPlugin",
ioExecutor, 1);
}
@Override
@@ -697,55 +697,46 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
private void updateConnectionStatus() {
ioExecutor.execute(() -> {
connectionStatusExecutor.execute(() -> {
if (!running) return;
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
boolean online = net != null && net.isConnected();
boolean wifi = online && net.getType() == TYPE_WIFI;
String country = locationUtils.getCurrentCountry();
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
country);
Settings s = callback.getSettings();
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
try {
connectionStatusLock.lock();
updateConnectionStatusLocked();
} finally {
connectionStatusLock.unlock();
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (blocked) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
LOG.info("Disabling network due to data setting");
enableNetwork(false);
} else {
LOG.info("Enabling network");
enableNetwork(true);
}
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
// Locking: connectionStatusLock
private void updateConnectionStatusLocked() {
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo();
boolean online = net != null && net.isConnected();
boolean wifi = online && net.getType() == TYPE_WIFI;
String country = locationUtils.getCurrentCountry();
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(country);
Settings s = callback.getSettings();
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi);
if ("".equals(country)) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
}
try {
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (blocked) {
LOG.info("Disabling network, country is blocked");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
LOG.info("Disabling network due to data setting");
enableNetwork(false);
} else {
LOG.info("Enabling network");
enableNetwork(true);
}
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
private void scheduleConnectionStatusUpdate() {
Future<?> newConnectivityCheck =
scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);
@@ -787,7 +778,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private synchronized void enableNetwork(boolean enable) {
networkEnabled = enable;
circuitBuilt = false;
if (!enable) circuitBuilt = false;
}
private synchronized boolean isConnected() {

View File

@@ -1,41 +1,18 @@
package org.briarproject.bramble.util;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.provider.Settings;
import java.io.File;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.WIFI_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.list;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.StringUtils.ipToString;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@SuppressLint("HardwareIds")
public class AndroidUtils {
// Fake Bluetooth address returned by BluetoothAdapter on API 23 and later
@@ -46,7 +23,7 @@ public class AndroidUtils {
@SuppressWarnings("deprecation")
public static Collection<String> getSupportedArchitectures() {
List<String> abis = new ArrayList<>();
if (SDK_INT >= 21) {
if (Build.VERSION.SDK_INT >= 21) {
abis.addAll(Arrays.asList(Build.SUPPORTED_ABIS));
} else {
abis.add(Build.CPU_ABI);
@@ -90,123 +67,4 @@ public class AndroidUtils {
public static File getReportDir(Context ctx) {
return ctx.getDir(STORED_REPORTS, MODE_PRIVATE);
}
public static void logNetworkState(Context ctx, Logger logger) {
if (!logger.isLoggable(INFO)) return;
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE);
if (o == null) throw new AssertionError();
ConnectivityManager cm = (ConnectivityManager) o;
o = ctx.getApplicationContext().getSystemService(WIFI_SERVICE);
if (o == null) throw new AssertionError();
WifiManager wm = (WifiManager) o;
StringBuilder s = new StringBuilder();
logWifiInfo(s, wm.getConnectionInfo());
logNetworkInfo(s, cm.getActiveNetworkInfo(), true);
if (SDK_INT >= 21) {
for (Network network : cm.getAllNetworks())
logNetworkInfo(s, cm.getNetworkInfo(network), false);
} else {
for (NetworkInfo info : cm.getAllNetworkInfo())
logNetworkInfo(s, info, false);
}
try {
for (NetworkInterface iface : list(getNetworkInterfaces()))
logNetworkInterface(s, iface);
} catch (SocketException e) {
logger.log(WARNING, e.toString(), e);
}
logger.log(INFO, s.toString());
}
private static void logWifiInfo(StringBuilder s, @Nullable WifiInfo info) {
if (info == null) {
s.append("Wifi info: null\n");
return;
}
s.append("Wifi info:\n");
s.append("\tSSID: ").append(info.getSSID()).append("\n");
s.append("\tBSSID: ").append(info.getBSSID()).append("\n");
s.append("\tMAC address: ").append(info.getMacAddress()).append("\n");
s.append("\tIP address: ")
.append(ipToString(info.getIpAddress())).append("\n");
s.append("\tSupplicant state: ")
.append(info.getSupplicantState()).append("\n");
s.append("\tNetwork ID: ").append(info.getNetworkId()).append("\n");
s.append("\tLink speed: ").append(info.getLinkSpeed()).append("\n");
s.append("\tRSSI: ").append(info.getRssi()).append("\n");
if (info.getHiddenSSID()) s.append("\tHidden SSID\n");
if (SDK_INT >= 21)
s.append("\tFrequency: ").append(info.getFrequency()).append("\n");
}
private static void logNetworkInfo(StringBuilder s,
@Nullable NetworkInfo info, boolean active) {
if (info == null) {
if (active) s.append("Active network info: null\n");
else s.append("Network info: null\n");
return;
}
if (active) s.append("Active network info:\n");
else s.append("Network info:\n");
s.append("\tType: ").append(info.getTypeName())
.append(" (").append(info.getType()).append(")\n");
s.append("\tSubtype: ").append(info.getSubtypeName())
.append(" (").append(info.getSubtype()).append(")\n");
s.append("\tState: ").append(info.getState()).append("\n");
s.append("\tDetailed state: ")
.append(info.getDetailedState()).append("\n");
s.append("\tReason: ").append(info.getReason()).append("\n");
s.append("\tExtra info: ").append(info.getExtraInfo()).append("\n");
if (info.isAvailable()) s.append("\tAvailable\n");
if (info.isConnected()) s.append("\tConnected\n");
if (info.isConnectedOrConnecting())
s.append("\tConnected or connecting\n");
if (info.isFailover()) s.append("\tFailover\n");
if (info.isRoaming()) s.append("\tRoaming\n");
}
private static void logNetworkInterface(StringBuilder s,
NetworkInterface iface) throws SocketException {
s.append("Network interface:\n");
s.append("\tName: ").append(iface.getName()).append("\n");
s.append("\tDisplay name: ")
.append(iface.getDisplayName()).append("\n");
s.append("\tHardware address: ")
.append(hexOrNull(iface.getHardwareAddress())).append("\n");
if (iface.isLoopback()) s.append("\tLoopback\n");
if (iface.isPointToPoint()) s.append("\tPoint-to-point\n");
if (iface.isVirtual()) s.append("\tVirtual\n");
if (iface.isUp()) s.append("\tUp\n");
if (SDK_INT >= 19)
s.append("\tIndex: ").append(iface.getIndex()).append("\n");
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
s.append("\tInterface address:\n");
logInetAddress(s, addr.getAddress());
s.append("\t\tPrefix length: ")
.append(addr.getNetworkPrefixLength()).append("\n");
}
}
private static void logInetAddress(StringBuilder s, InetAddress addr) {
s.append("\t\tAddress: ")
.append(hexOrNull(addr.getAddress())).append("\n");
s.append("\t\tHost address: ")
.append(addr.getHostAddress()).append("\n");
if (addr.isLoopbackAddress()) s.append("\t\tLoopback\n");
if (addr.isLinkLocalAddress()) s.append("\t\tLink-local\n");
if (addr.isSiteLocalAddress()) s.append("\t\tSite-local\n");
if (addr.isAnyLocalAddress()) s.append("\t\tAny local (wildcard)\n");
if (addr.isMCNodeLocal()) s.append("\t\tMulticast node-local\n");
if (addr.isMCLinkLocal()) s.append("\t\tMulticast link-local\n");
if (addr.isMCSiteLocal()) s.append("\t\tMulticast site-local\n");
if (addr.isMCOrgLocal()) s.append("\t\tMulticast org-local\n");
if (addr.isMCGlobal()) s.append("\t\tMulticast global\n");
}
@Nullable
private static String hexOrNull(@Nullable byte[] b) {
return b == null ? null : toHexString(b);
}
}

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
@@ -88,6 +90,10 @@ public interface ClientHelper {
BdfDictionary toDictionary(byte[] b, int off, int len)
throws FormatException;
BdfDictionary toDictionary(TransportProperties transportProperties);
BdfDictionary toDictionary(Map<TransportId, TransportProperties> map);
BdfList toList(byte[] b, int off, int len) throws FormatException;
BdfList toList(byte[] b) throws FormatException;
@@ -99,8 +105,15 @@ public interface ClientHelper {
byte[] sign(String label, BdfList toSign, byte[] privateKey)
throws FormatException, GeneralSecurityException;
void verifySignature(String label, byte[] sig, byte[] publicKey,
BdfList signed) throws FormatException, GeneralSecurityException;
void verifySignature(byte[] signature, String label, BdfList signed,
byte[] publicKey) throws FormatException, GeneralSecurityException;
Author parseAndValidateAuthor(BdfList author) throws FormatException;
TransportProperties parseAndValidateTransportProperties(
BdfDictionary properties) throws FormatException;
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException;
}

View File

@@ -6,7 +6,7 @@ import javax.annotation.concurrent.Immutable;
/**
* Type-safe wrapper for an integer that uniquely identifies a contact within
* the scope of a single node.
* the scope of the local device.
*/
@Immutable
@NotNullByDefault

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
@@ -13,23 +14,28 @@ import java.util.Collection;
public interface ContactManager {
/**
* Registers a hook to be called whenever a contact is added.
* Registers a hook to be called whenever a contact is added or removed.
* This method should be called before
* {@link LifecycleManager#startServices(String)}.
*/
void registerAddContactHook(AddContactHook hook);
void registerContactHook(ContactHook hook);
/**
* Registers a hook to be called whenever a contact is removed.
*/
void registerRemoveContactHook(RemoveContactHook hook);
/**
* Stores a contact within the given transaction associated with the given
* local and remote pseudonyms, and returns an ID for the contact.
* Stores a contact associated with the given local and remote pseudonyms,
* derives and stores transport keys for each transport, and returns an ID
* for the contact.
*/
ContactId addContact(Transaction txn, Author remote, AuthorId local,
SecretKey master, long timestamp, boolean alice, boolean verified,
boolean active) throws DbException;
/**
* Stores a contact associated with the given local and remote pseudonyms
* and returns an ID for the contact.
*/
ContactId addContact(Transaction txn, Author remote, AuthorId local,
boolean verified, boolean active) throws DbException;
/**
* Stores a contact associated with the given local and remote pseudonyms,
* and returns an ID for the contact.
@@ -94,11 +100,10 @@ public interface ContactManager {
boolean contactExists(AuthorId remoteAuthorId, AuthorId localAuthorId)
throws DbException;
interface AddContactHook {
void addingContact(Transaction txn, Contact c) throws DbException;
}
interface ContactHook {
void addingContact(Transaction txn, Contact c) throws DbException;
interface RemoveContactHook {
void removingContact(Transaction txn, Contact c) throws DbException;
}
}

View File

@@ -67,8 +67,8 @@ public interface CryptoComponent {
* signature created for another purpose
* @return true if the signature was valid, false otherwise.
*/
boolean verify(String label, byte[] signedData, byte[] publicKey,
byte[] signature) throws GeneralSecurityException;
boolean verifySignature(byte[] signature, String label, byte[] signed,
byte[] publicKey) throws GeneralSecurityException;
/**
* Returns the hash of the given inputs. The inputs are unambiguously
@@ -91,6 +91,18 @@ public interface CryptoComponent {
*/
byte[] mac(String label, SecretKey macKey, byte[]... inputs);
/**
* Verifies that the given message authentication code is valid for the
* given secret key and inputs.
*
* @param label a namespaced label indicating the purpose of this MAC, to
* prevent it from being repurposed or colliding with a MAC created for
* another purpose
* @return true if the MAC was valid, false otherwise.
*/
boolean verifyMac(byte[] mac, String label, SecretKey macKey,
byte[]... inputs);
/**
* Encrypts and authenticates the given plaintext so it can be written to
* storage. The encryption and authentication keys are derived from the

View File

@@ -16,4 +16,10 @@ public interface CryptoConstants {
* The maximum length of a signature in bytes.
*/
int MAX_SIGNATURE_BYTES = 64;
/**
* The length of a MAC in bytes.
*/
int MAC_BYTES = SecretKey.LENGTH;
}

View File

@@ -14,9 +14,10 @@ public interface TransportCrypto {
* rotation period from the given master secret.
*
* @param alice whether the keys are for use by Alice or Bob.
* @param active whether the keys are usable for outgoing streams.
*/
TransportKeys deriveTransportKeys(TransportId t, SecretKey master,
long rotationPeriod, boolean alice);
long rotationPeriod, boolean alice, boolean active);
/**
* Rotates the given transport keys to the given rotation period. If the

View File

@@ -34,7 +34,7 @@ public class BdfDictionary extends TreeMap<String, Object> {
super();
}
public BdfDictionary(Map<String, Object> m) {
public BdfDictionary(Map<String, ?> m) {
super(m);
}

View File

@@ -18,7 +18,8 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.ValidationManager;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
@@ -103,10 +104,17 @@ public interface DatabaseComponent {
throws DbException;
/**
* Stores transport keys for a newly added contact.
* Stores the given transport keys, optionally binding them to the given
* contact, and returns a key set ID.
*/
void addTransportKeys(Transaction txn, ContactId c, TransportKeys k)
throws DbException;
KeySetId addTransportKeys(Transaction txn, @Nullable ContactId c,
TransportKeys k) throws DbException;
/**
* Binds the given keys for the given transport to the given contact.
*/
void bindTransportKeys(Transaction txn, ContactId c, TransportId t,
KeySetId k) throws DbException;
/**
* Returns true if the database contains the given contact for the given
@@ -339,12 +347,8 @@ public interface DatabaseComponent {
/**
* Returns the IDs and states of all dependencies of the given message.
* Missing dependencies have the state
* {@link ValidationManager.State UNKNOWN}.
* Dependencies in other groups have the state
* {@link ValidationManager.State INVALID}.
* Note that these states are not set on the dependencies themselves; the
* returned states should only be taken in the context of the given message.
* For missing dependencies and dependencies in other groups, the state
* {@link State UNKNOWN} is returned.
* <p/>
* Read-only.
*/
@@ -352,9 +356,9 @@ public interface DatabaseComponent {
throws DbException;
/**
* Returns all IDs of messages that depend on the given message.
* Messages in other groups that declare a dependency on the given message
* will be returned even though such dependencies are invalid.
* Returns the IDs and states of all dependents of the given message.
* Dependents in other groups are not returned. If the given message is
* missing, no dependents are returned.
* <p/>
* Read-only.
*/
@@ -399,15 +403,14 @@ public interface DatabaseComponent {
* <p/>
* Read-only.
*/
Map<ContactId, TransportKeys> getTransportKeys(Transaction txn,
TransportId t) throws DbException;
Collection<KeySet> getTransportKeys(Transaction txn, TransportId t)
throws DbException;
/**
* Increments the outgoing stream counter for the given contact and
* transport in the given rotation period .
* Increments the outgoing stream counter for the given transport keys.
*/
void incrementStreamCounter(Transaction txn, ContactId c, TransportId t,
long rotationPeriod) throws DbException;
void incrementStreamCounter(Transaction txn, TransportId t, KeySetId k)
throws DbException;
/**
* Merges the given metadata with the existing metadata for the given
@@ -477,6 +480,12 @@ public interface DatabaseComponent {
*/
void removeTransport(Transaction txn, TransportId t) throws DbException;
/**
* Removes the given transport keys from the database.
*/
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
throws DbException;
/**
* Marks the given contact as verified.
*/
@@ -512,15 +521,21 @@ public interface DatabaseComponent {
Collection<MessageId> dependencies) throws DbException;
/**
* Sets the reordering window for the given contact and transport in the
* Sets the reordering window for the given key set and transport in the
* given rotation period.
*/
void setReorderingWindow(Transaction txn, ContactId c, TransportId t,
void setReorderingWindow(Transaction txn, KeySetId k, TransportId t,
long rotationPeriod, long base, byte[] bitmap) throws DbException;
/**
* Marks the given transport keys as usable for outgoing streams.
*/
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
throws DbException;
/**
* Stores the given transport keys, deleting any keys they have replaced.
*/
void updateTransportKeys(Transaction txn,
Map<ContactId, TransportKeys> keys) throws DbException;
void updateTransportKeys(Transaction txn, Collection<KeySet> keys)
throws DbException;
}

View File

@@ -1,22 +1,23 @@
package org.briarproject.bramble.api.plugin;
import java.nio.charset.Charset;
import org.briarproject.bramble.util.StringUtils;
/**
* Type-safe wrapper for a string that uniquely identifies a transport plugin.
* Type-safe wrapper for a namespaced string that uniquely identifies a
* transport plugin.
*/
public class TransportId {
/**
* The maximum length of transport identifier in UTF-8 bytes.
* The maximum length of a transport identifier in UTF-8 bytes.
*/
public static int MAX_TRANSPORT_ID_LENGTH = 64;
public static int MAX_TRANSPORT_ID_LENGTH = 100;
private final String id;
public TransportId(String id) {
byte[] b = id.getBytes(Charset.forName("UTF-8"));
if (b.length == 0 || b.length > MAX_TRANSPORT_ID_LENGTH)
int length = StringUtils.toUtf8(id).length;
if (length == 0 || length > MAX_TRANSPORT_ID_LENGTH)
throw new IllegalArgumentException();
this.id = id;
}

View File

@@ -1,19 +1,29 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import javax.annotation.concurrent.Immutable;
/**
* Wrapper for a name-spaced string that uniquely identifies a sync client.
* Type-safe wrapper for a namespaced string that uniquely identifies a sync
* client.
*/
@Immutable
@NotNullByDefault
public class ClientId implements Comparable<ClientId> {
/**
* The maximum length of a client identifier in UTF-8 bytes.
*/
public static int MAX_CLIENT_ID_LENGTH = 100;
private final String id;
public ClientId(String id) {
int length = StringUtils.toUtf8(id).length;
if (length == 0 || length > MAX_CLIENT_ID_LENGTH)
throw new IllegalArgumentException();
this.id = id;
}

View File

@@ -10,6 +10,11 @@ public class Group {
SHARED // The group is visible and messages are shared
}
/**
* The current version of the group format.
*/
public static final int FORMAT_VERSION = 1;
private final GroupId id;
private final ClientId clientId;
private final byte[] descriptor;

View File

@@ -5,6 +5,11 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LEN
public class Message {
/**
* The current version of the message format.
*/
public static final int FORMAT_VERSION = 1;
private final MessageId id;
private final GroupId groupId;
private final long timestamp;

View File

@@ -16,7 +16,13 @@ public class MessageId extends UniqueId {
/**
* Label for hashing messages to calculate their identifiers.
*/
public static final String LABEL = "org.briarproject.bramble/MESSAGE_ID";
public static final String ID_LABEL = "org.briarproject.bramble/MESSAGE_ID";
/**
* Label for hashing blocks of messages.
*/
public static final String BLOCK_LABEL =
"org.briarproject.bramble/MESSAGE_BLOCK";
public MessageId(byte[] id) {
super(id);

View File

@@ -6,6 +6,8 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.plugin.TransportId;
import java.util.Map;
import javax.annotation.Nullable;
/**
@@ -16,13 +18,51 @@ public interface KeyManager {
/**
* Informs the key manager that a new contact has been added. Derives and
* stores transport keys for communicating with the contact.
* stores a set of transport keys for communicating with the contact over
* each transport.
* <p/>
* {@link StreamContext StreamContexts} for the contact can be created
* after this method has returned.
*/
void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException;
/**
* Derives and stores a set of unbound transport keys for each transport
* and returns the key set IDs.
* <p/>
* The keys must be bound before they can be used for incoming streams,
* and also activated before they can be used for outgoing streams.
*/
Map<TransportId, KeySetId> addUnboundKeys(Transaction txn, SecretKey master,
long timestamp, boolean alice) throws DbException;
/**
* Binds the given transport keys to the given contact.
*/
void bindKeys(Transaction txn, ContactId c, Map<TransportId, KeySetId> keys)
throws DbException;
/**
* Marks the given transport keys as usable for outgoing streams. Keys must
* be bound before they are activated.
*/
void activateKeys(Transaction txn, Map<TransportId, KeySetId> keys)
throws DbException;
/**
* Removes the given transport keys, which must not have been bound, from
* the manager and the database.
*/
void removeKeys(Transaction txn, Map<TransportId, KeySetId> keys)
throws DbException;
/**
* Returns true if we have keys that can be used for outgoing streams to
* the given contact over the given transport.
*/
boolean canSendOutgoingStreams(ContactId c, TransportId t);
/**
* Returns a {@link StreamContext} for sending a stream to the given
* contact over the given transport, or null if an error occurs or the

View File

@@ -0,0 +1,51 @@
package org.briarproject.bramble.api.transport;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* A set of transport keys for communicating with a contact. If the keys have
* not yet been bound to a contact, {@link #getContactId()}} returns null.
*/
@Immutable
@NotNullByDefault
public class KeySet {
private final KeySetId keySetId;
@Nullable
private final ContactId contactId;
private final TransportKeys transportKeys;
public KeySet(KeySetId keySetId, @Nullable ContactId contactId,
TransportKeys transportKeys) {
this.keySetId = keySetId;
this.contactId = contactId;
this.transportKeys = transportKeys;
}
public KeySetId getKeySetId() {
return keySetId;
}
@Nullable
public ContactId getContactId() {
return contactId;
}
public TransportKeys getTransportKeys() {
return transportKeys;
}
@Override
public int hashCode() {
return keySetId.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof KeySet && keySetId.equals(((KeySet) o).keySetId);
}
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.bramble.api.transport;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* Type-safe wrapper for an integer that uniquely identifies a set of transport
* keys within the scope of the local device.
* <p/>
* Key sets created on a given device must have increasing identifiers.
*/
@Immutable
@NotNullByDefault
public class KeySetId {
private final int id;
public KeySetId(int id) {
this.id = id;
}
public int getInt() {
return id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object o) {
return o instanceof KeySetId && id == ((KeySetId) o).id;
}
}

View File

@@ -10,18 +10,20 @@ public class OutgoingKeys {
private final SecretKey tagKey, headerKey;
private final long rotationPeriod, streamCounter;
private final boolean active;
public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
long rotationPeriod) {
this(tagKey, headerKey, rotationPeriod, 0);
long rotationPeriod, boolean active) {
this(tagKey, headerKey, rotationPeriod, 0, active);
}
public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
long rotationPeriod, long streamCounter) {
long rotationPeriod, long streamCounter, boolean active) {
this.tagKey = tagKey;
this.headerKey = headerKey;
this.rotationPeriod = rotationPeriod;
this.streamCounter = streamCounter;
this.active = active;
}
public SecretKey getTagKey() {
@@ -39,4 +41,8 @@ public class OutgoingKeys {
public long getStreamCounter() {
return streamCounter;
}
public boolean isActive() {
return active;
}
}

View File

@@ -146,14 +146,6 @@ public class StringUtils {
return s.toString();
}
public static String ipToString(int ip) {
int ip1 = ip & 0xFF;
int ip2 = (ip >> 8) & 0xFF;
int ip3 = (ip >> 16) & 0xFF;
int ip4 = (ip >> 24) & 0xFF;
return ip1 + "." + ip2 + "." + ip3 + "." + ip4;
}
public static String getRandomString(int length) {
char[] c = new char[length];
for (int i = 0; i < length; i++)

View File

@@ -5,6 +5,8 @@ import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
@@ -16,13 +18,18 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@@ -54,6 +61,33 @@ public class TestUtils {
return getRandomBytes(UniqueId.LENGTH);
}
public static ClientId getClientId() {
return new ClientId(getRandomString(MAX_CLIENT_ID_LENGTH));
}
public static TransportId getTransportId() {
return new TransportId(getRandomString(MAX_TRANSPORT_ID_LENGTH));
}
public static TransportProperties getTransportProperties(int number) {
TransportProperties tp = new TransportProperties();
for (int i = 0; i < number; i++) {
tp.put(getRandomString(1 + random.nextInt(MAX_PROPERTY_LENGTH)),
getRandomString(1 + random.nextInt(MAX_PROPERTY_LENGTH))
);
}
return tp;
}
public static Map<TransportId, TransportProperties> getTransportPropertiesMap(
int number) {
Map<TransportId, TransportProperties> map = new HashMap<>();
for (int i = 0; i < number; i++) {
map.put(getTransportId(), getTransportProperties(number));
}
return map;
}
public static SecretKey getSecretKey() {
return new SecretKey(getRandomBytes(SecretKey.LENGTH));
}

View File

@@ -18,6 +18,8 @@ import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
@@ -37,6 +39,8 @@ import javax.inject.Inject;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
@@ -324,6 +328,20 @@ class ClientHelperImpl implements ClientHelper {
}
}
@Override
public BdfDictionary toDictionary(TransportProperties transportProperties) {
return new BdfDictionary(transportProperties);
}
@Override
public BdfDictionary toDictionary(
Map<TransportId, TransportProperties> map) {
BdfDictionary d = new BdfDictionary();
for (Entry<TransportId, TransportProperties> e : map.entrySet())
d.put(e.getKey().getString(), new BdfDictionary(e.getValue()));
return d;
}
@Override
public BdfList toList(byte[] b, int off, int len) throws FormatException {
ByteArrayInputStream in = new ByteArrayInputStream(b, off, len);
@@ -363,9 +381,10 @@ class ClientHelperImpl implements ClientHelper {
}
@Override
public void verifySignature(String label, byte[] sig, byte[] publicKey,
BdfList signed) throws FormatException, GeneralSecurityException {
if (!crypto.verify(label, toByteArray(signed), publicKey, sig)) {
public void verifySignature(byte[] signature, String label, BdfList signed,
byte[] publicKey) throws FormatException, GeneralSecurityException {
if (!crypto.verifySignature(signature, label, toByteArray(signed),
publicKey)) {
throw new GeneralSecurityException("Invalid signature");
}
}
@@ -382,4 +401,33 @@ class ClientHelperImpl implements ClientHelper {
checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH);
return authorFactory.createAuthor(formatVersion, name, publicKey);
}
@Override
public TransportProperties parseAndValidateTransportProperties(
BdfDictionary properties) throws FormatException {
checkSize(properties, 0, MAX_PROPERTIES_PER_TRANSPORT);
TransportProperties p = new TransportProperties();
for (String key : properties.keySet()) {
checkLength(key, 1, MAX_PROPERTY_LENGTH);
String value = properties.getString(key);
checkLength(value, 1, MAX_PROPERTY_LENGTH);
p.put(key, value);
}
return p;
}
@Override
public Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException {
Map<TransportId, TransportProperties> tpMap = new HashMap<>();
for (String key : properties.keySet()) {
TransportId transportId = new TransportId(key);
TransportProperties transportProperties =
parseAndValidateTransportProperties(
properties.getDictionary(key));
tpMap.put(transportId, transportProperties);
}
return tpMap;
}
}

View File

@@ -251,7 +251,8 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
r.readListEnd();
LOG.info("Received pseudonym");
// Verify the signature
if (!crypto.verify(SIGNING_LABEL_EXCHANGE, nonce, publicKey, sig)) {
if (!crypto.verifySignature(sig, SIGNING_LABEL_EXCHANGE, nonce,
publicKey)) {
if (LOG.isLoggable(INFO))
LOG.info("Invalid signature");
throw new GeneralSecurityException();

View File

@@ -27,36 +27,37 @@ class ContactManagerImpl implements ContactManager {
private final DatabaseComponent db;
private final KeyManager keyManager;
private final List<AddContactHook> addHooks;
private final List<RemoveContactHook> removeHooks;
private final List<ContactHook> hooks;
@Inject
ContactManagerImpl(DatabaseComponent db, KeyManager keyManager) {
this.db = db;
this.keyManager = keyManager;
addHooks = new CopyOnWriteArrayList<>();
removeHooks = new CopyOnWriteArrayList<>();
hooks = new CopyOnWriteArrayList<>();
}
@Override
public void registerAddContactHook(AddContactHook hook) {
addHooks.add(hook);
}
@Override
public void registerRemoveContactHook(RemoveContactHook hook) {
removeHooks.add(hook);
public void registerContactHook(ContactHook hook) {
hooks.add(hook);
}
@Override
public ContactId addContact(Transaction txn, Author remote, AuthorId local,
SecretKey master,long timestamp, boolean alice, boolean verified,
SecretKey master, long timestamp, boolean alice, boolean verified,
boolean active) throws DbException {
ContactId c = db.addContact(txn, remote, local, verified, active);
keyManager.addContact(txn, c, master, timestamp, alice);
Contact contact = db.getContact(txn, c);
for (AddContactHook hook : addHooks)
hook.addingContact(txn, contact);
for (ContactHook hook : hooks) hook.addingContact(txn, contact);
return c;
}
@Override
public ContactId addContact(Transaction txn, Author remote, AuthorId local,
boolean verified, boolean active) throws DbException {
ContactId c = db.addContact(txn, remote, local, verified, active);
Contact contact = db.getContact(txn, c);
for (ContactHook hook : hooks) hook.addingContact(txn, contact);
return c;
}
@@ -156,7 +157,7 @@ class ContactManagerImpl implements ContactManager {
@Override
public boolean contactExists(AuthorId remoteAuthorId,
AuthorId localAuthorId) throws DbException {
boolean exists = false;
boolean exists;
Transaction txn = db.startTransaction(true);
try {
exists = contactExists(txn, remoteAuthorId, localAuthorId);
@@ -171,8 +172,7 @@ class ContactManagerImpl implements ContactManager {
public void removeContact(Transaction txn, ContactId c)
throws DbException {
Contact contact = db.getContact(txn, c);
for (RemoveContactHook hook : removeHooks)
hook.removingContact(txn, contact);
for (ContactHook hook : hooks) hook.removingContact(txn, contact);
db.removeContact(txn, c);
}

View File

@@ -205,12 +205,12 @@ class CryptoComponentImpl implements CryptoComponent {
}
@Override
public boolean verify(String label, byte[] signedData, byte[] publicKey,
byte[] signature) throws GeneralSecurityException {
public boolean verifySignature(byte[] signature, String label,
byte[] signed, byte[] publicKey) throws GeneralSecurityException {
PublicKey key = signatureKeyParser.parsePublicKey(publicKey);
Signature sig = new EdSignature();
sig.initVerify(key);
updateSignature(sig, label, signedData);
updateSignature(sig, label, signed);
return sig.verify(signature);
}
@@ -262,6 +262,17 @@ class CryptoComponentImpl implements CryptoComponent {
return output;
}
@Override
public boolean verifyMac(byte[] mac, String label, SecretKey macKey,
byte[]... inputs) {
byte[] expected = mac(label, macKey, inputs);
if (mac.length != expected.length) return false;
// Constant-time comparison
int cmp = 0;
for (int i = 0; i < mac.length; i++) cmp |= mac[i] ^ expected[i];
return cmp == 0;
}
@Override
public byte[] encryptWithPassword(byte[] input, String password) {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();

View File

@@ -36,7 +36,8 @@ class TransportCryptoImpl implements TransportCrypto {
@Override
public TransportKeys deriveTransportKeys(TransportId t,
SecretKey master, long rotationPeriod, boolean alice) {
SecretKey master, long rotationPeriod, boolean alice,
boolean active) {
// Keys for the previous period are derived from the master secret
SecretKey inTagPrev = deriveTagKey(master, t, !alice);
SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice);
@@ -57,7 +58,7 @@ class TransportCryptoImpl implements TransportCrypto {
IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext,
rotationPeriod + 1);
OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr,
rotationPeriod);
rotationPeriod, active);
// Collect and return the keys
return new TransportKeys(t, inPrev, inCurr, inNext, outCurr);
}
@@ -71,6 +72,7 @@ class TransportCryptoImpl implements TransportCrypto {
IncomingKeys inNext = k.getNextIncomingKeys();
OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
long startPeriod = outCurr.getRotationPeriod();
boolean active = outCurr.isActive();
// Rotate the keys
for (long p = startPeriod + 1; p <= rotationPeriod; p++) {
inPrev = inCurr;
@@ -80,7 +82,7 @@ class TransportCryptoImpl implements TransportCrypto {
inNext = new IncomingKeys(inNextTag, inNextHeader, p + 1);
SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p);
SecretKey outCurrHeader = rotateKey(outCurr.getHeaderKey(), p);
outCurr = new OutgoingKeys(outCurrTag, outCurrHeader, p);
outCurr = new OutgoingKeys(outCurrTag, outCurrHeader, p, active);
}
// Collect and return the keys
return new TransportKeys(k.getTransportId(), inPrev, inCurr, inNext,

View File

@@ -21,6 +21,8 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
@@ -105,10 +107,11 @@ interface Database<T> {
@Nullable ContactId sender) throws DbException;
/**
* Adds a dependency between two messages in the given group.
* Adds a dependency between two messages, where the dependent message is
* in the given state.
*/
void addMessageDependency(T txn, GroupId g, MessageId dependent,
MessageId dependency) throws DbException;
void addMessageDependency(T txn, Message dependent, MessageId dependency,
State dependentState) throws DbException;
/**
* Records that a message has been offered by the given contact.
@@ -122,9 +125,16 @@ interface Database<T> {
throws DbException;
/**
* Stores transport keys for a newly added contact.
* Stores the given transport keys, optionally binding them to the given
* contact, and returns a key set ID.
*/
void addTransportKeys(T txn, ContactId c, TransportKeys k)
KeySetId addTransportKeys(T txn, @Nullable ContactId c, TransportKeys k)
throws DbException;
/**
* Binds the given keys for the given transport to the given contact.
*/
void bindTransportKeys(T txn, ContactId c, TransportId t, KeySetId k)
throws DbException;
/**
@@ -292,10 +302,8 @@ interface Database<T> {
/**
* Returns the IDs and states of all dependencies of the given message.
* Missing dependencies have the state {@link State UNKNOWN}.
* Dependencies in other groups have the state {@link State INVALID}.
* Note that these states are not set on the dependencies themselves; the
* returned states should only be taken in the context of the given message.
* For missing dependencies and dependencies in other groups, the state
* {@link State UNKNOWN} is returned.
* <p/>
* Read-only.
*/
@@ -303,9 +311,9 @@ interface Database<T> {
throws DbException;
/**
* Returns all IDs and states of all dependents of the given message.
* Messages in other groups that declare a dependency on the given message
* will be returned even though such dependencies are invalid.
* Returns the IDs and states of all dependents of the given message.
* Dependents in other groups are not returned. If the given message is
* missing, no dependents are returned.
* <p/>
* Read-only.
*/
@@ -487,15 +495,14 @@ interface Database<T> {
* <p/>
* Read-only.
*/
Map<ContactId, TransportKeys> getTransportKeys(T txn, TransportId t)
Collection<KeySet> getTransportKeys(T txn, TransportId t)
throws DbException;
/**
* Increments the outgoing stream counter for the given contact and
* transport in the given rotation period.
* Increments the outgoing stream counter for the given transport keys.
*/
void incrementStreamCounter(T txn, ContactId c, TransportId t,
long rotationPeriod) throws DbException;
void incrementStreamCounter(T txn, TransportId t, KeySetId k)
throws DbException;
/**
* Marks the given messages as not needing to be acknowledged to the
@@ -585,6 +592,12 @@ interface Database<T> {
*/
void removeTransport(T txn, TransportId t) throws DbException;
/**
* Removes the given transport keys from the database.
*/
void removeTransportKeys(T txn, TransportId t, KeySetId k)
throws DbException;
/**
* Resets the transmission count and expiry time of the given message with
* respect to the given contact.
@@ -620,12 +633,18 @@ interface Database<T> {
void setMessageState(T txn, MessageId m, State state) throws DbException;
/**
* Sets the reordering window for the given contact and transport in the
* Sets the reordering window for the given key set and transport in the
* given rotation period.
*/
void setReorderingWindow(T txn, ContactId c, TransportId t,
void setReorderingWindow(T txn, KeySetId k, TransportId t,
long rotationPeriod, long base, byte[] bitmap) throws DbException;
/**
* Marks the given transport keys as usable for outgoing streams.
*/
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
throws DbException;
/**
* Updates the transmission count and expiry time of the given message
* with respect to the given contact, using the latency of the transport
@@ -637,6 +656,5 @@ interface Database<T> {
/**
* Stores the given transport keys, deleting any keys they have replaced.
*/
void updateTransportKeys(T txn, Map<ContactId, TransportKeys> keys)
throws DbException;
void updateTransportKeys(T txn, Collection<KeySet> keys) throws DbException;
}

View File

@@ -51,15 +51,15 @@ import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Logger;
@@ -234,15 +234,27 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public void addTransportKeys(Transaction transaction, ContactId c,
TransportKeys k) throws DbException {
public KeySetId addTransportKeys(Transaction transaction,
@Nullable ContactId c, TransportKeys k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (c != null && !db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, k.getTransportId()))
throw new NoSuchTransportException();
return db.addTransportKeys(txn, c, k);
}
@Override
public void bindTransportKeys(Transaction transaction, ContactId c,
TransportId t, KeySetId k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, k.getTransportId()))
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
db.addTransportKeys(txn, c, k);
db.bindTransportKeys(txn, c, t, k);
}
@Override
@@ -586,8 +598,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public Map<ContactId, TransportKeys> getTransportKeys(
Transaction transaction, TransportId t) throws DbException {
public Collection<KeySet> getTransportKeys(Transaction transaction,
TransportId t) throws DbException {
T txn = unbox(transaction);
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
@@ -595,15 +607,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public void incrementStreamCounter(Transaction transaction, ContactId c,
TransportId t, long rotationPeriod) throws DbException {
public void incrementStreamCounter(Transaction transaction, TransportId t,
KeySetId k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
db.incrementStreamCounter(txn, c, t, rotationPeriod);
db.incrementStreamCounter(txn, t, k);
}
@Override
@@ -765,6 +775,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
// TODO: Don't allow messages with dependents to be removed
db.removeMessage(txn, m);
}
@@ -778,6 +789,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.removeTransport(txn, t);
}
@Override
public void removeTransportKeys(Transaction transaction,
TransportId t, KeySetId k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
db.removeTransportKeys(txn, t, k);
}
@Override
public void setContactVerified(Transaction transaction, ContactId c)
throws DbException {
@@ -850,38 +871,42 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction);
if (!db.containsMessage(txn, dependent.getId()))
throw new NoSuchMessageException();
State dependentState = db.getMessageState(txn, dependent.getId());
for (MessageId dependency : dependencies) {
db.addMessageDependency(txn, dependent.getGroupId(),
dependent.getId(), dependency);
db.addMessageDependency(txn, dependent, dependency, dependentState);
}
}
@Override
public void setReorderingWindow(Transaction transaction, ContactId c,
public void setReorderingWindow(Transaction transaction, KeySetId k,
TransportId t, long rotationPeriod, long base, byte[] bitmap)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
db.setReorderingWindow(txn, c, t, rotationPeriod, base, bitmap);
db.setReorderingWindow(txn, k, t, rotationPeriod, base, bitmap);
}
@Override
public void setTransportKeysActive(Transaction transaction, TransportId t,
KeySetId k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsTransport(txn, t))
throw new NoSuchTransportException();
db.setTransportKeysActive(txn, t, k);
}
@Override
public void updateTransportKeys(Transaction transaction,
Map<ContactId, TransportKeys> keys) throws DbException {
Collection<KeySet> keys) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
Map<ContactId, TransportKeys> filtered = new HashMap<>();
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
ContactId c = e.getKey();
TransportKeys k = e.getValue();
if (db.containsContact(txn, c)
&& db.containsTransport(txn, k.getTransportId())) {
filtered.put(c, k);
}
Collection<KeySet> filtered = new ArrayList<>();
for (KeySet ks : keys) {
TransportId t = ks.getTransportKeys().getTransportId();
if (db.containsTransport(txn, t)) filtered.add(ks);
}
db.updateTransportKeys(txn, filtered);
}

View File

@@ -25,6 +25,8 @@ import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys;
@@ -50,6 +52,7 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.sql.Types.INTEGER;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
@@ -57,7 +60,6 @@ import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
@@ -72,7 +74,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
static final int CODE_SCHEMA_VERSION = 35;
static final int CODE_SCHEMA_VERSION = 36;
private static final String CREATE_SETTINGS =
"CREATE TABLE settings"
@@ -170,6 +172,10 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " (groupId _HASH NOT NULL,"
+ " messageId _HASH NOT NULL,"
+ " dependencyId _HASH NOT NULL," // Not a foreign key
+ " messageState INT NOT NULL," // Denormalised
// Denormalised, null if dependency is missing or in a
// different group
+ " dependencyState INT,"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE,"
@@ -219,37 +225,44 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " maxLatency INT NOT NULL,"
+ " PRIMARY KEY (transportId))";
private static final String CREATE_OUTGOING_KEYS =
"CREATE TABLE outgoingKeys"
+ " (transportId _STRING NOT NULL,"
+ " keySetId _COUNTER,"
+ " rotationPeriod BIGINT NOT NULL,"
+ " contactId INT," // Null if keys are not bound
+ " tagKey _SECRET NOT NULL,"
+ " headerKey _SECRET NOT NULL,"
+ " stream BIGINT NOT NULL,"
+ " active BOOLEAN NOT NULL,"
+ " PRIMARY KEY (transportId, keySetId),"
+ " FOREIGN KEY (transportId)"
+ " REFERENCES transports (transportId)"
+ " ON DELETE CASCADE,"
+ " UNIQUE (keySetId),"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_INCOMING_KEYS =
"CREATE TABLE incomingKeys"
+ " (contactId INT NOT NULL,"
+ " transportId _STRING NOT NULL,"
+ " (transportId _STRING NOT NULL,"
+ " keySetId INT NOT NULL,"
+ " rotationPeriod BIGINT NOT NULL,"
+ " contactId INT," // Null if keys are not bound
+ " tagKey _SECRET NOT NULL,"
+ " headerKey _SECRET NOT NULL,"
+ " base BIGINT NOT NULL,"
+ " bitmap _BINARY NOT NULL,"
+ " PRIMARY KEY (contactId, transportId, rotationPeriod),"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE,"
+ " PRIMARY KEY (transportId, keySetId, rotationPeriod),"
+ " FOREIGN KEY (transportId)"
+ " REFERENCES transports (transportId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_OUTGOING_KEYS =
"CREATE TABLE outgoingKeys"
+ " (contactId INT NOT NULL,"
+ " transportId _STRING NOT NULL,"
+ " rotationPeriod BIGINT NOT NULL,"
+ " tagKey _SECRET NOT NULL,"
+ " headerKey _SECRET NOT NULL,"
+ " stream BIGINT NOT NULL,"
+ " PRIMARY KEY (contactId, transportId),"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (keySetId)"
+ " REFERENCES outgoingKeys (keySetId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (transportId)"
+ " REFERENCES transports (transportId)"
+ " ON DELETE CASCADE)";
private static final String INDEX_CONTACTS_BY_AUTHOR_ID =
@@ -264,6 +277,10 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
+ " ON messageMetadata (groupId, state)";
private static final String INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID =
"CREATE INDEX IF NOT EXISTS messageDependenciesByDependencyId"
+ " ON messageDependencies (dependencyId)";
private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID =
"CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId"
+ " ON statuses (contactId, groupId)";
@@ -407,8 +424,8 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(insertTypeNames(CREATE_OFFERS));
s.executeUpdate(insertTypeNames(CREATE_STATUSES));
s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
s.executeUpdate(insertTypeNames(CREATE_INCOMING_KEYS));
s.executeUpdate(insertTypeNames(CREATE_OUTGOING_KEYS));
s.executeUpdate(insertTypeNames(CREATE_INCOMING_KEYS));
s.close();
} catch (SQLException e) {
tryToClose(s);
@@ -423,6 +440,7 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
s.close();
@@ -715,6 +733,17 @@ abstract class JdbcDatabase implements Database<Connection> {
m.getLength(), state, e.getValue(), messageShared,
false, seen);
}
// Update denormalised column in messageDependencies if dependency
// is in same group as dependent
sql = "UPDATE messageDependencies SET dependencyState = ?"
+ " WHERE groupId = ? AND dependencyId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setBytes(2, m.getGroupId().getBytes());
ps.setBytes(3, m.getId().getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -784,21 +813,42 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void addMessageDependency(Connection txn, GroupId g,
MessageId dependent, MessageId dependency) throws DbException {
public void addMessageDependency(Connection txn, Message dependent,
MessageId dependency, State dependentState) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "INSERT INTO messageDependencies"
+ " (groupId, messageId, dependencyId)"
+ " VALUES (?, ?, ?)";
// Get state of dependency if present and in same group as dependent
String sql = "SELECT state FROM messages"
+ " WHERE messageId = ? AND groupId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
ps.setBytes(2, dependent.getBytes());
ps.setBytes(1, dependency.getBytes());
ps.setBytes(2, dependent.getGroupId().getBytes());
rs = ps.executeQuery();
State dependencyState = null;
if (rs.next()) {
dependencyState = State.fromValue(rs.getInt(1));
if (rs.next()) throw new DbStateException();
}
rs.close();
ps.close();
// Create messageDependencies row
sql = "INSERT INTO messageDependencies"
+ " (groupId, messageId, dependencyId, messageState,"
+ " dependencyState)"
+ " VALUES (?, ?, ?, ? ,?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, dependent.getGroupId().getBytes());
ps.setBytes(2, dependent.getId().getBytes());
ps.setBytes(3, dependency.getBytes());
ps.setInt(4, dependentState.getValue());
if (dependencyState == null) ps.setNull(5, INTEGER);
else ps.setInt(5, dependencyState.getValue());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
@@ -824,61 +874,105 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void addTransportKeys(Connection txn, ContactId c, TransportKeys k)
throws DbException {
public KeySetId addTransportKeys(Connection txn, @Nullable ContactId c,
TransportKeys k) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Store the incoming keys
String sql = "INSERT INTO incomingKeys (contactId, transportId,"
+ " rotationPeriod, tagKey, headerKey, base, bitmap)"
// Store the outgoing keys
String sql = "INSERT INTO outgoingKeys (contactId, transportId,"
+ " rotationPeriod, tagKey, headerKey, stream, active)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setString(2, k.getTransportId().getString());
// Previous rotation period
IncomingKeys inPrev = k.getPreviousIncomingKeys();
ps.setLong(3, inPrev.getRotationPeriod());
ps.setBytes(4, inPrev.getTagKey().getBytes());
ps.setBytes(5, inPrev.getHeaderKey().getBytes());
ps.setLong(6, inPrev.getWindowBase());
ps.setBytes(7, inPrev.getWindowBitmap());
ps.addBatch();
// Current rotation period
IncomingKeys inCurr = k.getCurrentIncomingKeys();
ps.setLong(3, inCurr.getRotationPeriod());
ps.setBytes(4, inCurr.getTagKey().getBytes());
ps.setBytes(5, inCurr.getHeaderKey().getBytes());
ps.setLong(6, inCurr.getWindowBase());
ps.setBytes(7, inCurr.getWindowBitmap());
ps.addBatch();
// Next rotation period
IncomingKeys inNext = k.getNextIncomingKeys();
ps.setLong(3, inNext.getRotationPeriod());
ps.setBytes(4, inNext.getTagKey().getBytes());
ps.setBytes(5, inNext.getHeaderKey().getBytes());
ps.setLong(6, inNext.getWindowBase());
ps.setBytes(7, inNext.getWindowBitmap());
ps.addBatch();
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != 3) throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
// Store the outgoing keys
sql = "INSERT INTO outgoingKeys (contactId, transportId,"
+ " rotationPeriod, tagKey, headerKey, stream)"
+ " VALUES (?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
if (c == null) ps.setNull(1, INTEGER);
else ps.setInt(1, c.getInt());
ps.setString(2, k.getTransportId().getString());
OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
ps.setLong(3, outCurr.getRotationPeriod());
ps.setBytes(4, outCurr.getTagKey().getBytes());
ps.setBytes(5, outCurr.getHeaderKey().getBytes());
ps.setLong(6, outCurr.getStreamCounter());
ps.setBoolean(7, outCurr.isActive());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Get the new (highest) key set ID
sql = "SELECT keySetId FROM outgoingKeys"
+ " ORDER BY keySetId DESC LIMIT 1";
ps = txn.prepareStatement(sql);
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
KeySetId keySetId = new KeySetId(rs.getInt(1));
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
// Store the incoming keys
sql = "INSERT INTO incomingKeys (keySetId, contactId, transportId,"
+ " rotationPeriod, tagKey, headerKey, base, bitmap)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, keySetId.getInt());
if (c == null) ps.setNull(2, INTEGER);
else ps.setInt(2, c.getInt());
ps.setString(3, k.getTransportId().getString());
// Previous rotation period
IncomingKeys inPrev = k.getPreviousIncomingKeys();
ps.setLong(4, inPrev.getRotationPeriod());
ps.setBytes(5, inPrev.getTagKey().getBytes());
ps.setBytes(6, inPrev.getHeaderKey().getBytes());
ps.setLong(7, inPrev.getWindowBase());
ps.setBytes(8, inPrev.getWindowBitmap());
ps.addBatch();
// Current rotation period
IncomingKeys inCurr = k.getCurrentIncomingKeys();
ps.setLong(4, inCurr.getRotationPeriod());
ps.setBytes(5, inCurr.getTagKey().getBytes());
ps.setBytes(6, inCurr.getHeaderKey().getBytes());
ps.setLong(7, inCurr.getWindowBase());
ps.setBytes(8, inCurr.getWindowBitmap());
ps.addBatch();
// Next rotation period
IncomingKeys inNext = k.getNextIncomingKeys();
ps.setLong(4, inNext.getRotationPeriod());
ps.setBytes(5, inNext.getTagKey().getBytes());
ps.setBytes(6, inNext.getHeaderKey().getBytes());
ps.setLong(7, inNext.getWindowBase());
ps.setBytes(8, inNext.getWindowBitmap());
ps.addBatch();
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != 3) throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
return keySetId;
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
@Override
public void bindTransportKeys(Connection txn, ContactId c, TransportId t,
KeySetId k) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE outgoingKeys SET contactId = ?"
+ " WHERE keySetId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, k.getInt());
int affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
sql = "UPDATE incomingKeys SET contactId = ?"
+ " WHERE keySetId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, k.getInt());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -1663,11 +1757,9 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT d.dependencyId, m.state, d.groupId, m.groupId"
+ " FROM messageDependencies AS d"
+ " LEFT OUTER JOIN messages AS m"
+ " ON d.dependencyId = m.messageId"
+ " WHERE d.messageId = ?";
String sql = "SELECT dependencyId, dependencyState"
+ " FROM messageDependencies"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
@@ -1675,14 +1767,8 @@ abstract class JdbcDatabase implements Database<Connection> {
while (rs.next()) {
MessageId dependency = new MessageId(rs.getBytes(1));
State state = State.fromValue(rs.getInt(2));
if (rs.wasNull()) {
state = UNKNOWN; // Missing dependency
} else {
GroupId dependentGroupId = new GroupId(rs.getBytes(3));
GroupId dependencyGroupId = new GroupId(rs.getBytes(4));
if (!dependentGroupId.equals(dependencyGroupId))
state = INVALID; // Dependency in another group
}
if (rs.wasNull())
state = UNKNOWN; // Missing or in a different group
dependencies.put(dependency, state);
}
rs.close();
@@ -1701,11 +1787,12 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT d.messageId, m.state"
+ " FROM messageDependencies AS d"
+ " JOIN messages AS m"
+ " ON d.messageId = m.messageId"
+ " WHERE dependencyId = ?";
// Exclude dependencies that are missing or in a different group
// from the dependent
String sql = "SELECT messageId, messageState"
+ " FROM messageDependencies"
+ " WHERE dependencyId = ?"
+ " AND dependencyState IS NOT NULL";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
@@ -2044,8 +2131,8 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public Map<ContactId, TransportKeys> getTransportKeys(Connection txn,
TransportId t) throws DbException {
public Collection<KeySet> getTransportKeys(Connection txn, TransportId t)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
@@ -2054,7 +2141,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " base, bitmap"
+ " FROM incomingKeys"
+ " WHERE transportId = ?"
+ " ORDER BY contactId, rotationPeriod";
+ " ORDER BY keySetId, rotationPeriod";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
rs = ps.executeQuery();
@@ -2071,29 +2158,34 @@ abstract class JdbcDatabase implements Database<Connection> {
rs.close();
ps.close();
// Retrieve the outgoing keys in the same order
sql = "SELECT contactId, rotationPeriod, tagKey, headerKey, stream"
sql = "SELECT keySetId, contactId, rotationPeriod,"
+ " tagKey, headerKey, stream, active"
+ " FROM outgoingKeys"
+ " WHERE transportId = ?"
+ " ORDER BY contactId, rotationPeriod";
+ " ORDER BY keySetId";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
rs = ps.executeQuery();
Map<ContactId, TransportKeys> keys = new HashMap<>();
Collection<KeySet> keys = new ArrayList<>();
for (int i = 0; rs.next(); i++) {
// There should be three times as many incoming keys
if (inKeys.size() < (i + 1) * 3) throw new DbStateException();
ContactId contactId = new ContactId(rs.getInt(1));
long rotationPeriod = rs.getLong(2);
SecretKey tagKey = new SecretKey(rs.getBytes(3));
SecretKey headerKey = new SecretKey(rs.getBytes(4));
long streamCounter = rs.getLong(5);
KeySetId keySetId = new KeySetId(rs.getInt(1));
ContactId contactId = new ContactId(rs.getInt(2));
if (rs.wasNull()) contactId = null;
long rotationPeriod = rs.getLong(3);
SecretKey tagKey = new SecretKey(rs.getBytes(4));
SecretKey headerKey = new SecretKey(rs.getBytes(5));
long streamCounter = rs.getLong(6);
boolean active = rs.getBoolean(7);
OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
rotationPeriod, streamCounter);
rotationPeriod, streamCounter, active);
IncomingKeys inPrev = inKeys.get(i * 3);
IncomingKeys inCurr = inKeys.get(i * 3 + 1);
IncomingKeys inNext = inKeys.get(i * 3 + 2);
keys.put(contactId, new TransportKeys(t, inPrev, inCurr,
inNext, outCurr));
TransportKeys transportKeys = new TransportKeys(t, inPrev,
inCurr, inNext, outCurr);
keys.add(new KeySet(keySetId, contactId, transportKeys));
}
rs.close();
ps.close();
@@ -2106,17 +2198,15 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void incrementStreamCounter(Connection txn, ContactId c,
TransportId t, long rotationPeriod) throws DbException {
public void incrementStreamCounter(Connection txn, TransportId t,
KeySetId k) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE outgoingKeys SET stream = stream + 1"
+ " WHERE contactId = ? AND transportId = ?"
+ " AND rotationPeriod = ?";
+ " WHERE transportId = ? AND keySetId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setString(2, t.getString());
ps.setLong(3, rotationPeriod);
ps.setString(1, t.getString());
ps.setInt(2, k.getInt());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
@@ -2592,6 +2682,27 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void removeTransportKeys(Connection txn, TransportId t, KeySetId k)
throws DbException {
PreparedStatement ps = null;
try {
// Delete any existing outgoing keys - this will also remove any
// incoming keys with the same key set ID
String sql = "DELETE FROM outgoingKeys"
+ " WHERE transportId = ? AND keySetId = ?";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
ps.setInt(2, k.getInt());
int affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
@Override
public void resetExpiryTime(Connection txn, ContactId c, MessageId m)
throws DbException {
@@ -2731,6 +2842,25 @@ abstract class JdbcDatabase implements Database<Connection> {
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
// Update denormalised column in messageDependencies
sql = "UPDATE messageDependencies SET messageState = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
// Update denormalised column in messageDependencies if dependency
// is present and in same group as dependent
sql = "UPDATE messageDependencies SET dependencyState = ?"
+ " WHERE dependencyId = ? AND dependencyState IS NOT NULL";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
@@ -2738,18 +2868,18 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void setReorderingWindow(Connection txn, ContactId c, TransportId t,
public void setReorderingWindow(Connection txn, KeySetId k, TransportId t,
long rotationPeriod, long base, byte[] bitmap) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?"
+ " WHERE contactId = ? AND transportId = ?"
+ " WHERE transportId = ? AND keySetId = ?"
+ " AND rotationPeriod = ?";
ps = txn.prepareStatement(sql);
ps.setLong(1, base);
ps.setBytes(2, bitmap);
ps.setInt(3, c.getInt());
ps.setString(4, t.getString());
ps.setString(3, t.getString());
ps.setInt(4, k.getInt());
ps.setLong(5, rotationPeriod);
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
@@ -2760,6 +2890,25 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void setTransportKeysActive(Connection txn, TransportId t,
KeySetId k) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE outgoingKeys SET active = true"
+ " WHERE transportId = ? AND keySetId = ?";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
ps.setInt(2, k.getInt());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
@Override
public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException {
@@ -2795,45 +2944,12 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void updateTransportKeys(Connection txn,
Map<ContactId, TransportKeys> keys) throws DbException {
PreparedStatement ps = null;
try {
// Delete any existing incoming keys
String sql = "DELETE FROM incomingKeys"
+ " WHERE contactId = ?"
+ " AND transportId = ?";
ps = txn.prepareStatement(sql);
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
ps.setInt(1, e.getKey().getInt());
ps.setString(2, e.getValue().getTransportId().getString());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != keys.size())
throw new DbStateException();
ps.close();
// Delete any existing outgoing keys
sql = "DELETE FROM outgoingKeys"
+ " WHERE contactId = ?"
+ " AND transportId = ?";
ps = txn.prepareStatement(sql);
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
ps.setInt(1, e.getKey().getInt());
ps.setString(2, e.getValue().getTransportId().getString());
ps.addBatch();
}
batchAffected = ps.executeBatch();
if (batchAffected.length != keys.size())
throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
// Store the new keys
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
addTransportKeys(txn, e.getKey(), e.getValue());
public void updateTransportKeys(Connection txn, Collection<KeySet> keys)
throws DbException {
for (KeySet ks : keys) {
TransportKeys k = ks.getTransportKeys();
removeTransportKeys(txn, k.getTransportId(), ks.getKeySetId());
addTransportKeys(txn, ks.getContactId(), k);
}
}
}

View File

@@ -241,10 +241,11 @@ class LanTcpPlugin extends TcpPlugin {
}
return null;
}
Socket s = new Socket();
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
@@ -47,7 +48,7 @@ abstract class TcpPlugin implements DuplexPlugin {
private static final Logger LOG =
Logger.getLogger(TcpPlugin.class.getName());
protected final Executor ioExecutor;
protected final Executor ioExecutor, bindExecutor;
protected final Backoff backoff;
protected final DuplexPluginCallback callback;
protected final int maxLatency, maxIdleTime, socketTimeout;
@@ -90,6 +91,8 @@ abstract class TcpPlugin implements DuplexPlugin {
if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
// Don't execute more than one bind operation at a time
bindExecutor = new PoliteExecutor("TcpPlugin", ioExecutor, 1);
}
@Override
@@ -110,8 +113,9 @@ abstract class TcpPlugin implements DuplexPlugin {
}
protected void bind() {
ioExecutor.execute(() -> {
bindExecutor.execute(() -> {
if (!running) return;
if (socket != null && !socket.isClosed()) return;
ServerSocket ss = null;
for (InetSocketAddress addr : getLocalSocketAddresses()) {
try {
@@ -243,10 +247,11 @@ abstract class TcpPlugin implements DuplexPlugin {
}
continue;
}
Socket s = new Socket();
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
@@ -261,6 +266,10 @@ abstract class TcpPlugin implements DuplexPlugin {
return null;
}
protected Socket createSocket() throws IOException {
return new Socket();
}
@Nullable
InetSocketAddress parseSocketAddress(String ipPort) {
if (StringUtils.isNullOrEmpty(ipPort)) return null;

View File

@@ -46,8 +46,7 @@ public class PropertiesModule {
lifecycleManager.registerClient(transportPropertyManager);
validationManager.registerIncomingMessageHook(CLIENT_ID,
transportPropertyManager);
contactManager.registerAddContactHook(transportPropertyManager);
contactManager.registerRemoveContactHook(transportPropertyManager);
contactManager.registerContactHook(transportPropertyManager);
return transportPropertyManager;
}
}

View File

@@ -5,8 +5,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.AddContactHook;
import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook;
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser;
@@ -40,7 +39,7 @@ import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@Immutable
@NotNullByDefault
class TransportPropertyManagerImpl implements TransportPropertyManager,
Client, AddContactHook, RemoveContactHook, IncomingMessageHook {
Client, ContactHook, IncomingMessageHook {
private final DatabaseComponent db;
private final ClientHelper clientHelper;
@@ -348,10 +347,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
throws FormatException {
// Transport ID, version, properties
BdfDictionary dictionary = message.getDictionary(2);
TransportProperties p = new TransportProperties();
for (String key : dictionary.keySet())
p.put(key, dictionary.getString(key));
return p;
return clientHelper.parseAndValidateTransportProperties(dictionary);
}
private static class LatestUpdate {

View File

@@ -15,8 +15,6 @@ import org.briarproject.bramble.api.system.Clock;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
@@ -42,12 +40,7 @@ class TransportPropertyValidator extends BdfMessageValidator {
if (version < 0) throw new FormatException();
// Properties
BdfDictionary dictionary = body.getDictionary(2);
checkSize(dictionary, 0, MAX_PROPERTIES_PER_TRANSPORT);
for (String key : dictionary.keySet()) {
checkLength(key, 0, MAX_PROPERTY_LENGTH);
String value = dictionary.getString(key);
checkLength(value, 0, MAX_PROPERTY_LENGTH);
}
clientHelper.parseAndValidateTransportProperties(dictionary);
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put("transportId", transportId);

View File

@@ -12,8 +12,8 @@ import org.briarproject.bramble.util.StringUtils;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.Group.FORMAT_VERSION;
import static org.briarproject.bramble.api.sync.GroupId.LABEL;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
@Immutable
@@ -31,7 +31,7 @@ class GroupFactoryImpl implements GroupFactory {
public Group createGroup(ClientId c, int clientVersion, byte[] descriptor) {
byte[] clientVersionBytes = new byte[INT_32_BYTES];
ByteUtils.writeUint32(clientVersion, clientVersionBytes, 0);
byte[] hash = crypto.hash(LABEL, new byte[] {PROTOCOL_VERSION},
byte[] hash = crypto.hash(LABEL, new byte[] {FORMAT_VERSION},
StringUtils.toUtf8(c.getString()), clientVersionBytes,
descriptor);
return new Group(new GroupId(hash), c, descriptor);

View File

@@ -12,10 +12,12 @@ import org.briarproject.bramble.util.ByteUtils;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.MessageId.LABEL;
import static org.briarproject.bramble.api.sync.Message.FORMAT_VERSION;
import static org.briarproject.bramble.api.sync.MessageId.BLOCK_LABEL;
import static org.briarproject.bramble.api.sync.MessageId.ID_LABEL;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES;
@Immutable
@NotNullByDefault
@@ -32,11 +34,14 @@ class MessageFactoryImpl implements MessageFactory {
public Message createMessage(GroupId g, long timestamp, byte[] body) {
if (body.length > MAX_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException();
byte[] timeBytes = new byte[ByteUtils.INT_64_BYTES];
byte[] versionBytes = new byte[] {FORMAT_VERSION};
// There's only one block, so the root hash is the hash of the block
byte[] rootHash = crypto.hash(BLOCK_LABEL, versionBytes, body);
byte[] timeBytes = new byte[INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] hash = crypto.hash(LABEL, new byte[] {PROTOCOL_VERSION},
g.getBytes(), timeBytes, body);
MessageId id = new MessageId(hash);
byte[] idHash = crypto.hash(ID_LABEL, versionBytes, g.getBytes(),
timeBytes, rootHash);
MessageId id = new MessageId(idHash);
byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length];
System.arraycopy(g.getBytes(), 0, raw, 0, UniqueId.LENGTH);
ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH);

View File

@@ -4,8 +4,9 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -25,7 +26,10 @@ public class SystemModule {
private final ScheduledExecutorService scheduler;
public SystemModule() {
scheduler = Executors.newSingleThreadScheduledExecutor();
// Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ScheduledThreadPoolExecutor.DiscardPolicy();
scheduler = new ScheduledThreadPoolExecutor(1, policy);
}
@Provides

View File

@@ -19,6 +19,7 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext;
import java.util.HashMap;
@@ -104,6 +105,67 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
m.addContact(txn, c, master, timestamp, alice);
}
@Override
public Map<TransportId, KeySetId> addUnboundKeys(Transaction txn,
SecretKey master, long timestamp, boolean alice)
throws DbException {
Map<TransportId, KeySetId> ids = new HashMap<>();
for (Entry<TransportId, TransportKeyManager> e : managers.entrySet()) {
TransportId t = e.getKey();
TransportKeyManager m = e.getValue();
ids.put(t, m.addUnboundKeys(txn, master, timestamp, alice));
}
return ids;
}
@Override
public void bindKeys(Transaction txn, ContactId c,
Map<TransportId, KeySetId> keys) throws DbException {
for (Entry<TransportId, KeySetId> e : keys.entrySet()) {
TransportId t = e.getKey();
TransportKeyManager m = managers.get(t);
if (m == null) {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
} else {
m.bindKeys(txn, c, e.getValue());
}
}
}
@Override
public void activateKeys(Transaction txn, Map<TransportId, KeySetId> keys)
throws DbException {
for (Entry<TransportId, KeySetId> e : keys.entrySet()) {
TransportId t = e.getKey();
TransportKeyManager m = managers.get(t);
if (m == null) {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
} else {
m.activateKeys(txn, e.getValue());
}
}
}
@Override
public void removeKeys(Transaction txn, Map<TransportId, KeySetId> keys)
throws DbException {
for (Entry<TransportId, KeySetId> e : keys.entrySet()) {
TransportId t = e.getKey();
TransportKeyManager m = managers.get(t);
if (m == null) {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
} else {
m.removeKeys(txn, e.getValue());
}
}
}
@Override
public boolean canSendOutgoingStreams(ContactId c, TransportId t) {
TransportKeyManager m = managers.get(t);
return m == null ? false : m.canSendOutgoingStreams(c);
}
@Override
public StreamContext getStreamContext(ContactId c, TransportId t)
throws DbException {
@@ -114,7 +176,7 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
return null;
}
StreamContext ctx = null;
StreamContext ctx;
Transaction txn = db.startTransaction(false);
try {
ctx = m.getStreamContext(txn, c);
@@ -133,7 +195,7 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
return null;
}
StreamContext ctx = null;
StreamContext ctx;
Transaction txn = db.startTransaction(false);
try {
ctx = m.getStreamContext(txn, tag);

View File

@@ -0,0 +1,34 @@
package org.briarproject.bramble.transport;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.transport.KeySetId;
import javax.annotation.Nullable;
public class MutableKeySet {
private final KeySetId keySetId;
@Nullable
private final ContactId contactId;
private final MutableTransportKeys transportKeys;
public MutableKeySet(KeySetId keySetId, @Nullable ContactId contactId,
MutableTransportKeys transportKeys) {
this.keySetId = keySetId;
this.contactId = contactId;
this.transportKeys = transportKeys;
}
public KeySetId getKeySetId() {
return keySetId;
}
@Nullable
public ContactId getContactId() {
return contactId;
}
public MutableTransportKeys getTransportKeys() {
return transportKeys;
}
}

View File

@@ -13,17 +13,19 @@ class MutableOutgoingKeys {
private final SecretKey tagKey, headerKey;
private final long rotationPeriod;
private long streamCounter;
private boolean active;
MutableOutgoingKeys(OutgoingKeys out) {
tagKey = out.getTagKey();
headerKey = out.getHeaderKey();
rotationPeriod = out.getRotationPeriod();
streamCounter = out.getStreamCounter();
active = out.isActive();
}
OutgoingKeys snapshot() {
return new OutgoingKeys(tagKey, headerKey, rotationPeriod,
streamCounter);
streamCounter, active);
}
SecretKey getTagKey() {
@@ -45,4 +47,12 @@ class MutableOutgoingKeys {
void incrementStreamCounter() {
streamCounter++;
}
boolean isActive() {
return active;
}
void activate() {
active = true;
}
}

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext;
import javax.annotation.Nullable;
@@ -17,8 +18,19 @@ interface TransportKeyManager {
void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException;
KeySetId addUnboundKeys(Transaction txn, SecretKey master, long timestamp,
boolean alice) throws DbException;
void bindKeys(Transaction txn, ContactId c, KeySetId k) throws DbException;
void activateKeys(Transaction txn, KeySetId k) throws DbException;
void removeKeys(Transaction txn, KeySetId k) throws DbException;
void removeContact(ContactId c);
boolean canSendOutgoingStreams(ContactId c);
@Nullable
StreamContext getStreamContext(Transaction txn, ContactId c)
throws DbException;

View File

@@ -11,19 +11,24 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.transport.ReorderingWindow.Change;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -47,12 +52,13 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private final Clock clock;
private final TransportId transportId;
private final long rotationPeriodLength;
private final ReentrantLock lock;
private final AtomicBoolean used = new AtomicBoolean(false);
private final ReentrantLock lock = new ReentrantLock();
// The following are locking: lock
private final Map<Bytes, TagContext> inContexts;
private final Map<ContactId, MutableOutgoingKeys> outContexts;
private final Map<ContactId, MutableTransportKeys> keys;
private final Map<KeySetId, MutableKeySet> keys = new HashMap<>();
private final Map<Bytes, TagContext> inContexts = new HashMap<>();
private final Map<ContactId, MutableKeySet> outContexts = new HashMap<>();
TransportKeyManagerImpl(DatabaseComponent db,
TransportCrypto transportCrypto, Executor dbExecutor,
@@ -65,20 +71,16 @@ class TransportKeyManagerImpl implements TransportKeyManager {
this.clock = clock;
this.transportId = transportId;
rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
lock = new ReentrantLock();
inContexts = new HashMap<>();
outContexts = new HashMap<>();
keys = new HashMap<>();
}
@Override
public void start(Transaction txn) throws DbException {
if (used.getAndSet(true)) throw new IllegalStateException();
long now = clock.currentTimeMillis();
lock.lock();
try {
// Load the transport keys from the DB
Map<ContactId, TransportKeys> loaded =
db.getTransportKeys(txn, transportId);
Collection<KeySet> loaded = db.getTransportKeys(txn, transportId);
// Rotate the keys to the current rotation period
RotationResult rotationResult = rotateKeys(loaded, now);
// Initialise mutable state for all contacts
@@ -93,41 +95,48 @@ class TransportKeyManagerImpl implements TransportKeyManager {
scheduleKeyRotation(now);
}
private RotationResult rotateKeys(Map<ContactId, TransportKeys> keys,
long now) {
private RotationResult rotateKeys(Collection<KeySet> keys, long now) {
RotationResult rotationResult = new RotationResult();
long rotationPeriod = now / rotationPeriodLength;
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
ContactId c = e.getKey();
TransportKeys k = e.getValue();
for (KeySet ks : keys) {
TransportKeys k = ks.getTransportKeys();
TransportKeys k1 =
transportCrypto.rotateTransportKeys(k, rotationPeriod);
KeySet ks1 = new KeySet(ks.getKeySetId(), ks.getContactId(), k1);
if (k1.getRotationPeriod() > k.getRotationPeriod())
rotationResult.rotated.put(c, k1);
rotationResult.current.put(c, k1);
rotationResult.rotated.add(ks1);
rotationResult.current.add(ks1);
}
return rotationResult;
}
// Locking: lock
private void addKeys(Map<ContactId, TransportKeys> m) {
for (Entry<ContactId, TransportKeys> e : m.entrySet())
addKeys(e.getKey(), new MutableTransportKeys(e.getValue()));
private void addKeys(Collection<KeySet> keys) {
for (KeySet ks : keys) {
addKeys(ks.getKeySetId(), ks.getContactId(),
new MutableTransportKeys(ks.getTransportKeys()));
}
}
// Locking: lock
private void addKeys(ContactId c, MutableTransportKeys m) {
encodeTags(c, m.getPreviousIncomingKeys());
encodeTags(c, m.getCurrentIncomingKeys());
encodeTags(c, m.getNextIncomingKeys());
outContexts.put(c, m.getCurrentOutgoingKeys());
keys.put(c, m);
private void addKeys(KeySetId keySetId, @Nullable ContactId contactId,
MutableTransportKeys m) {
MutableKeySet ks = new MutableKeySet(keySetId, contactId, m);
keys.put(keySetId, ks);
if (contactId != null) {
encodeTags(keySetId, contactId, m.getPreviousIncomingKeys());
encodeTags(keySetId, contactId, m.getCurrentIncomingKeys());
encodeTags(keySetId, contactId, m.getNextIncomingKeys());
considerReplacingOutgoingKeys(ks);
}
}
// Locking: lock
private void encodeTags(ContactId c, MutableIncomingKeys inKeys) {
private void encodeTags(KeySetId keySetId, ContactId contactId,
MutableIncomingKeys inKeys) {
for (long streamNumber : inKeys.getWindow().getUnseen()) {
TagContext tagCtx = new TagContext(c, inKeys, streamNumber);
TagContext tagCtx =
new TagContext(keySetId, contactId, inKeys, streamNumber);
byte[] tag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(tag, inKeys.getTagKey(), PROTOCOL_VERSION,
streamNumber);
@@ -135,6 +144,17 @@ class TransportKeyManagerImpl implements TransportKeyManager {
}
}
// Locking: lock
private void considerReplacingOutgoingKeys(MutableKeySet ks) {
// Use the active outgoing keys with the highest key set ID
if (ks.getTransportKeys().getCurrentOutgoingKeys().isActive()) {
MutableKeySet old = outContexts.get(ks.getContactId());
if (old == null ||
old.getKeySetId().getInt() < ks.getKeySetId().getInt())
outContexts.put(ks.getContactId(), ks);
}
}
private void scheduleKeyRotation(long now) {
long delay = rotationPeriodLength - now % rotationPeriodLength;
scheduler.schedule((Runnable) this::rotateKeys, delay, MILLISECONDS);
@@ -159,20 +179,82 @@ class TransportKeyManagerImpl implements TransportKeyManager {
@Override
public void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException {
deriveAndAddKeys(txn, c, master, timestamp, alice, true);
}
@Override
public KeySetId addUnboundKeys(Transaction txn, SecretKey master,
long timestamp, boolean alice) throws DbException {
return deriveAndAddKeys(txn, null, master, timestamp, alice, false);
}
private KeySetId deriveAndAddKeys(Transaction txn, @Nullable ContactId c,
SecretKey master, long timestamp, boolean alice, boolean active)
throws DbException {
lock.lock();
try {
// Work out what rotation period the timestamp belongs to
long rotationPeriod = timestamp / rotationPeriodLength;
// Derive the transport keys
TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
master, rotationPeriod, alice);
master, rotationPeriod, alice, active);
// Rotate the keys to the current rotation period if necessary
rotationPeriod = clock.currentTimeMillis() / rotationPeriodLength;
k = transportCrypto.rotateTransportKeys(k, rotationPeriod);
// Initialise mutable state for the contact
addKeys(c, new MutableTransportKeys(k));
// Write the keys back to the DB
db.addTransportKeys(txn, c, k);
KeySetId keySetId = db.addTransportKeys(txn, c, k);
// Initialise mutable state for the contact
addKeys(keySetId, c, new MutableTransportKeys(k));
return keySetId;
} finally {
lock.unlock();
}
}
@Override
public void bindKeys(Transaction txn, ContactId c, KeySetId k)
throws DbException {
lock.lock();
try {
MutableKeySet ks = keys.get(k);
if (ks == null) throw new IllegalArgumentException();
// Check that the keys haven't already been bound
if (ks.getContactId() != null) throw new IllegalArgumentException();
MutableTransportKeys m = ks.getTransportKeys();
addKeys(k, c, m);
db.bindTransportKeys(txn, c, m.getTransportId(), k);
} finally {
lock.unlock();
}
}
@Override
public void activateKeys(Transaction txn, KeySetId k) throws DbException {
lock.lock();
try {
MutableKeySet ks = keys.get(k);
if (ks == null) throw new IllegalArgumentException();
// Check that the keys have been bound
if (ks.getContactId() == null) throw new IllegalArgumentException();
MutableTransportKeys m = ks.getTransportKeys();
m.getCurrentOutgoingKeys().activate();
considerReplacingOutgoingKeys(ks);
db.setTransportKeysActive(txn, m.getTransportId(), k);
} finally {
lock.unlock();
}
}
@Override
public void removeKeys(Transaction txn, KeySetId k) throws DbException {
lock.lock();
try {
MutableKeySet ks = keys.remove(k);
if (ks == null) throw new IllegalArgumentException();
// Check that the keys haven't been bound
if (ks.getContactId() != null) throw new IllegalArgumentException();
TransportId t = ks.getTransportKeys().getTransportId();
db.removeTransportKeys(txn, t, k);
} finally {
lock.unlock();
}
@@ -183,12 +265,29 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock();
try {
// Remove mutable state for the contact
Iterator<Entry<Bytes, TagContext>> it =
inContexts.entrySet().iterator();
while (it.hasNext())
if (it.next().getValue().contactId.equals(c)) it.remove();
Iterator<TagContext> it = inContexts.values().iterator();
while (it.hasNext()) if (it.next().contactId.equals(c)) it.remove();
outContexts.remove(c);
keys.remove(c);
Iterator<MutableKeySet> it1 = keys.values().iterator();
while (it1.hasNext()) {
ContactId c1 = it1.next().getContactId();
if (c1 != null && c1.equals(c)) it1.remove();
}
} finally {
lock.unlock();
}
}
@Override
public boolean canSendOutgoingStreams(ContactId c) {
lock.lock();
try {
MutableKeySet ks = outContexts.get(c);
if (ks == null) return false;
MutableOutgoingKeys outKeys =
ks.getTransportKeys().getCurrentOutgoingKeys();
if (!outKeys.isActive()) throw new AssertionError();
return outKeys.getStreamCounter() <= MAX_32_BIT_UNSIGNED;
} finally {
lock.unlock();
}
@@ -200,8 +299,11 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock();
try {
// Look up the outgoing keys for the contact
MutableOutgoingKeys outKeys = outContexts.get(c);
if (outKeys == null) return null;
MutableKeySet ks = outContexts.get(c);
if (ks == null) return null;
MutableOutgoingKeys outKeys =
ks.getTransportKeys().getCurrentOutgoingKeys();
if (!outKeys.isActive()) throw new AssertionError();
if (outKeys.getStreamCounter() > MAX_32_BIT_UNSIGNED) return null;
// Create a stream context
StreamContext ctx = new StreamContext(c, transportId,
@@ -209,8 +311,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
outKeys.getStreamCounter());
// Increment the stream counter and write it back to the DB
outKeys.incrementStreamCounter();
db.incrementStreamCounter(txn, c, transportId,
outKeys.getRotationPeriod());
db.incrementStreamCounter(txn, transportId, ks.getKeySetId());
return ctx;
} finally {
lock.unlock();
@@ -238,8 +339,9 @@ class TransportKeyManagerImpl implements TransportKeyManager {
byte[] addTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(addTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber);
inContexts.put(new Bytes(addTag), new TagContext(
tagCtx.contactId, inKeys, streamNumber));
TagContext tagCtx1 = new TagContext(tagCtx.keySetId,
tagCtx.contactId, inKeys, streamNumber);
inContexts.put(new Bytes(addTag), tagCtx1);
}
// Remove tags for any stream numbers removed from the window
for (long streamNumber : change.getRemoved()) {
@@ -250,9 +352,19 @@ class TransportKeyManagerImpl implements TransportKeyManager {
inContexts.remove(new Bytes(removeTag));
}
// Write the window back to the DB
db.setReorderingWindow(txn, tagCtx.contactId, transportId,
db.setReorderingWindow(txn, tagCtx.keySetId, transportId,
inKeys.getRotationPeriod(), window.getBase(),
window.getBitmap());
// If the outgoing keys are inactive, activate them
MutableKeySet ks = keys.get(tagCtx.keySetId);
MutableOutgoingKeys outKeys =
ks.getTransportKeys().getCurrentOutgoingKeys();
if (!outKeys.isActive()) {
LOG.info("Activating outgoing keys");
outKeys.activate();
considerReplacingOutgoingKeys(ks);
db.setTransportKeysActive(txn, transportId, tagCtx.keySetId);
}
return ctx;
} finally {
lock.unlock();
@@ -264,9 +376,11 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock();
try {
// Rotate the keys to the current rotation period
Map<ContactId, TransportKeys> snapshot = new HashMap<>();
for (Entry<ContactId, MutableTransportKeys> e : keys.entrySet())
snapshot.put(e.getKey(), e.getValue().snapshot());
Collection<KeySet> snapshot = new ArrayList<>(keys.size());
for (MutableKeySet ks : keys.values()) {
snapshot.add(new KeySet(ks.getKeySetId(), ks.getContactId(),
ks.getTransportKeys().snapshot()));
}
RotationResult rotationResult = rotateKeys(snapshot, now);
// Rebuild the mutable state for all contacts
inContexts.clear();
@@ -285,12 +399,14 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private static class TagContext {
private final KeySetId keySetId;
private final ContactId contactId;
private final MutableIncomingKeys inKeys;
private final long streamNumber;
private TagContext(ContactId contactId, MutableIncomingKeys inKeys,
long streamNumber) {
private TagContext(KeySetId keySetId, ContactId contactId,
MutableIncomingKeys inKeys, long streamNumber) {
this.keySetId = keySetId;
this.contactId = contactId;
this.inKeys = inKeys;
this.streamNumber = streamNumber;
@@ -299,11 +415,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private static class RotationResult {
private final Map<ContactId, TransportKeys> current, rotated;
private RotationResult() {
current = new HashMap<>();
rotated = new HashMap<>();
}
private final Collection<KeySet> current = new ArrayList<>();
private final Collection<KeySet> rotated = new ArrayList<>();
}
}

View File

@@ -37,6 +37,7 @@ import java.util.Random;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
@@ -300,30 +301,34 @@ public class ClientHelperImplTest extends BrambleTestCase {
@Test
public void testVerifySignature() throws Exception {
byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH);
byte[] publicKey = getRandomBytes(42);
byte[] bytes = expectToByteArray(list);
byte[] signed = expectToByteArray(list);
context.checking(new Expectations() {{
oneOf(cryptoComponent).verify(label, bytes, publicKey, rawMessage);
oneOf(cryptoComponent).verifySignature(signature, label, signed,
publicKey);
will(returnValue(true));
}});
clientHelper.verifySignature(label, rawMessage, publicKey, list);
clientHelper.verifySignature(signature, label, list, publicKey);
context.assertIsSatisfied();
}
@Test
public void testVerifyWrongSignature() throws Exception {
byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH);
byte[] publicKey = getRandomBytes(42);
byte[] bytes = expectToByteArray(list);
byte[] signed = expectToByteArray(list);
context.checking(new Expectations() {{
oneOf(cryptoComponent).verify(label, bytes, publicKey, rawMessage);
oneOf(cryptoComponent).verifySignature(signature, label, signed,
publicKey);
will(returnValue(false));
}});
try {
clientHelper.verifySignature(label, rawMessage, publicKey, list);
clientHelper.verifySignature(signature, label, list, publicKey);
fail();
} catch (GeneralSecurityException e) {
// expected

View File

@@ -143,9 +143,9 @@ public class EdSignatureTest extends SignatureTest {
}
@Override
protected boolean verify(String label, byte[] signedData, byte[] publicKey,
byte[] signature) throws GeneralSecurityException {
return crypto.verify(label, signedData, publicKey, signature);
protected boolean verify(byte[] signature, String label, byte[] signed,
byte[] publicKey) throws GeneralSecurityException {
return crypto.verifySignature(signature, label, signed, publicKey);
}
@Test

View File

@@ -17,6 +17,7 @@ import java.util.List;
import java.util.Set;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -27,13 +28,13 @@ public class KeyDerivationTest extends BrambleTestCase {
new CryptoComponentImpl(new TestSecureRandomProvider(), null);
private final TransportCrypto transportCrypto =
new TransportCryptoImpl(crypto);
private final TransportId transportId = new TransportId("id");
private final TransportId transportId = getTransportId();
private final SecretKey master = getSecretKey();
@Test
public void testKeysAreDistinct() {
TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
assertAllDifferent(k);
}
@@ -41,9 +42,9 @@ public class KeyDerivationTest extends BrambleTestCase {
public void testCurrentKeysMatchCurrentKeysOfContact() {
// Start in rotation period 123
TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
master, 123, false);
master, 123, false, true);
// Alice's incoming keys should equal Bob's outgoing keys
assertArrayEquals(kA.getCurrentIncomingKeys().getTagKey().getBytes(),
kB.getCurrentOutgoingKeys().getTagKey().getBytes());
@@ -73,9 +74,9 @@ public class KeyDerivationTest extends BrambleTestCase {
public void testPreviousKeysMatchPreviousKeysOfContact() {
// Start in rotation period 123
TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
master, 123, false);
master, 123, false, true);
// Compare Alice's previous keys in period 456 with Bob's current keys
// in period 455
kA = transportCrypto.rotateTransportKeys(kA, 456);
@@ -100,9 +101,9 @@ public class KeyDerivationTest extends BrambleTestCase {
public void testNextKeysMatchNextKeysOfContact() {
// Start in rotation period 123
TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
master, 123, false);
master, 123, false, true);
// Compare Alice's current keys in period 456 with Bob's next keys in
// period 455
kA = transportCrypto.rotateTransportKeys(kA, 456);
@@ -127,20 +128,20 @@ public class KeyDerivationTest extends BrambleTestCase {
SecretKey master1 = getSecretKey();
assertFalse(Arrays.equals(master.getBytes(), master1.getBytes()));
TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
TransportKeys k1 = transportCrypto.deriveTransportKeys(transportId,
master1, 123, true);
master1, 123, true, true);
assertAllDifferent(k, k1);
}
@Test
public void testTransportIdAffectsOutput() {
TransportId transportId1 = new TransportId("id1");
TransportId transportId1 = getTransportId();
assertFalse(transportId.getString().equals(transportId1.getString()));
TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
master, 123, true);
master, 123, true, true);
TransportKeys k1 = transportCrypto.deriveTransportKeys(transportId1,
master, 123, true);
master, 123, true, true);
assertAllDifferent(k, k1);
}

View File

@@ -126,13 +126,13 @@ public class KeyEncodingAndParsingTest extends BrambleTestCase {
byte[] signature = crypto.sign("test", message,
privateKey.getEncoded());
// Verify the signature
assertTrue(crypto.verify("test", message, publicKey.getEncoded(),
signature));
assertTrue(crypto.verifySignature(signature, "test", message,
publicKey.getEncoded()));
// Encode and parse the public key - no exceptions should be thrown
publicKey = parser.parsePublicKey(publicKey.getEncoded());
// Verify the signature again
assertTrue(crypto.verify("test", message, publicKey.getEncoded(),
signature));
assertTrue(crypto.verifySignature(signature, "test", message,
publicKey.getEncoded()));
}
@Test
@@ -146,15 +146,15 @@ public class KeyEncodingAndParsingTest extends BrambleTestCase {
byte[] signature = crypto.sign("test", message,
privateKey.getEncoded());
// Verify the signature
assertTrue(crypto.verify("test", message, publicKey.getEncoded(),
signature));
assertTrue(crypto.verifySignature(signature, "test", message,
publicKey.getEncoded()));
// Encode and parse the private key - no exceptions should be thrown
privateKey = parser.parsePrivateKey(privateKey.getEncoded());
// Sign the data again - the signatures should be the same
byte[] signature1 = crypto.sign("test", message,
privateKey.getEncoded());
assertTrue(crypto.verify("test", message, publicKey.getEncoded(),
signature1));
assertTrue(crypto.verifySignature(signature1, "test", message,
publicKey.getEncoded()));
assertArrayEquals(signature, signature1);
}

View File

@@ -13,6 +13,7 @@ import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MacTest extends BrambleTestCase {
@@ -32,6 +33,7 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key1, input1, input2, input3);
assertArrayEquals(mac, mac1);
assertTrue(crypto.verifyMac(mac, label1, key1, input1, input2, input3));
}
@Test
@@ -40,6 +42,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label2, key1, input1, input2, input3);
assertFalse(Arrays.equals(mac, mac1));
// Each MAC should fail to verify with the other MAC's label
assertFalse(crypto.verifyMac(mac, label2, key1, input1, input2,
input3));
assertFalse(crypto.verifyMac(mac1, label1, key2, input1, input2,
input3));
}
@Test
@@ -48,6 +55,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key2, input1, input2, input3);
assertFalse(Arrays.equals(mac, mac1));
// Each MAC should fail to verify with the other MAC's key
assertFalse(crypto.verifyMac(mac, label1, key2, input1, input2,
input3));
assertFalse(crypto.verifyMac(mac1, label2, key1, input1, input2,
input3));
}
@Test
@@ -57,6 +69,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key1, input3, input2, input1);
assertFalse(Arrays.equals(mac, mac1));
// Each MAC should fail to verify with the other MAC's inputs
assertFalse(crypto.verifyMac(mac, label1, key2, input3, input2,
input1));
assertFalse(crypto.verifyMac(mac1, label1, key1, input1, input2,
input3));
}
}

View File

@@ -28,8 +28,8 @@ public abstract class SignatureTest extends BrambleTestCase {
protected abstract byte[] sign(String label, byte[] toSign,
byte[] privateKey) throws GeneralSecurityException;
protected abstract boolean verify(String label, byte[] signedData,
byte[] publicKey, byte[] signature) throws GeneralSecurityException;
protected abstract boolean verify(byte[] signature, String label,
byte[] signed, byte[] publicKey) throws GeneralSecurityException;
SignatureTest() {
crypto = new CryptoComponentImpl(new TestSecureRandomProvider(), null);
@@ -85,7 +85,7 @@ public abstract class SignatureTest extends BrambleTestCase {
@Test
public void testSignatureVerification() throws Exception {
byte[] sig = sign(label, inputBytes, privateKey);
assertTrue(verify(label, inputBytes, publicKey, sig));
assertTrue(verify(sig, label, inputBytes, publicKey));
}
@Test
@@ -95,7 +95,7 @@ public abstract class SignatureTest extends BrambleTestCase {
byte[] privateKey2 = k2.getPrivate().getEncoded();
// calculate the signature with different key, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey2);
assertFalse(verify(label, inputBytes, publicKey, sig));
assertFalse(verify(sig, label, inputBytes, publicKey));
}
@Test
@@ -104,7 +104,7 @@ public abstract class SignatureTest extends BrambleTestCase {
byte[] inputBytes2 = TestUtils.getRandomBytes(123);
// calculate the signature with different input, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey);
assertFalse(verify(label, inputBytes2, publicKey, sig));
assertFalse(verify(sig, label, inputBytes2, publicKey));
}
@Test
@@ -113,7 +113,7 @@ public abstract class SignatureTest extends BrambleTestCase {
String label2 = StringUtils.getRandomString(42);
// calculate the signature with different label, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey);
assertFalse(verify(label2, inputBytes, publicKey, sig));
assertFalse(verify(sig, label2, inputBytes, publicKey));
}
}

View File

@@ -44,34 +44,36 @@ import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -100,27 +102,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
private final int maxLatency;
private final ContactId contactId;
private final Contact contact;
private final KeySetId keySetId;
public DatabaseComponentImplTest() {
clientId = new ClientId(getRandomString(123));
groupId = new GroupId(TestUtils.getRandomId());
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
group = new Group(groupId, clientId, descriptor);
clientId = getClientId();
group = getGroup(clientId);
groupId = group.getId();
author = getAuthor();
localAuthor = getLocalAuthor();
messageId = new MessageId(TestUtils.getRandomId());
messageId1 = new MessageId(TestUtils.getRandomId());
messageId = new MessageId(getRandomId());
messageId1 = new MessageId(getRandomId());
long timestamp = System.currentTimeMillis();
size = 1234;
raw = new byte[size];
message = new Message(messageId, groupId, timestamp, raw);
metadata = new Metadata();
metadata.put("foo", new byte[] {'b', 'a', 'r'});
transportId = new TransportId("id");
transportId = getTransportId();
maxLatency = Integer.MAX_VALUE;
contactId = new ContactId(234);
contact = new Contact(contactId, author, localAuthor.getId(),
true, true);
keySetId = new KeySetId(345);
}
private DatabaseComponent createDatabaseComponent(Database<Object> database,
@@ -282,11 +285,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not)
exactly(18).of(database).startTransaction();
exactly(17).of(database).startTransaction();
will(returnValue(txn));
exactly(18).of(database).containsContact(txn, contactId);
exactly(17).of(database).containsContact(txn, contactId);
will(returnValue(false));
exactly(18).of(database).abortTransaction(txn);
exactly(17).of(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown);
@@ -301,6 +304,16 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.bindTransportKeys(transaction, contactId, transportId, keySetId);
fail();
} catch (NoSuchContactException expected) {
// Expected
} finally {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.generateAck(transaction, contactId, 123);
@@ -371,16 +384,6 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.incrementStreamCounter(transaction, contactId, transportId, 0);
fail();
} catch (NoSuchContactException expected) {
// Expected
} finally {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.getGroupVisibility(transaction, contactId, groupId);
@@ -454,17 +457,6 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.setReorderingWindow(transaction, contactId, transportId, 0, 0,
new byte[REORDERING_WINDOW_SIZE / 8]);
fail();
} catch (NoSuchContactException expected) {
// Expected
} finally {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.setGroupVisibility(transaction, contactId, groupId, SHARED);
@@ -777,13 +769,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// endTransaction()
oneOf(database).commitTransaction(txn);
// Check whether the transport is in the DB (which it's not)
exactly(4).of(database).startTransaction();
exactly(6).of(database).startTransaction();
will(returnValue(txn));
exactly(2).of(database).containsContact(txn, contactId);
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
exactly(4).of(database).containsTransport(txn, transportId);
exactly(6).of(database).containsTransport(txn, transportId);
will(returnValue(false));
exactly(4).of(database).abortTransaction(txn);
exactly(6).of(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown);
@@ -798,6 +790,16 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.bindTransportKeys(transaction, contactId, transportId, keySetId);
fail();
} catch (NoSuchTransportException expected) {
// Expected
} finally {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.getTransportKeys(transaction, transportId);
@@ -810,7 +812,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false);
try {
db.incrementStreamCounter(transaction, contactId, transportId, 0);
db.incrementStreamCounter(transaction, transportId, keySetId);
fail();
} catch (NoSuchTransportException expected) {
// Expected
@@ -830,7 +832,17 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false);
try {
db.setReorderingWindow(transaction, contactId, transportId, 0, 0,
db.removeTransportKeys(transaction, transportId, keySetId);
fail();
} catch (NoSuchTransportException expected) {
// Expected
} finally {
db.endTransaction(transaction);
}
transaction = db.startTransaction(false);
try {
db.setReorderingWindow(transaction, keySetId, transportId, 0, 0,
new byte[REORDERING_WINDOW_SIZE / 8]);
fail();
} catch (NoSuchTransportException expected) {
@@ -907,7 +919,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test
public void testGenerateOffer() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
context.checking(new Expectations() {{
oneOf(database).startTransaction();
@@ -938,7 +950,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test
public void testGenerateRequest() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
context.checking(new Expectations() {{
oneOf(database).startTransaction();
@@ -1126,9 +1138,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test
public void testReceiveOffer() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
MessageId messageId1 = new MessageId(getRandomId());
MessageId messageId2 = new MessageId(getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
@@ -1303,15 +1315,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test
public void testTransportKeys() throws Exception {
TransportKeys transportKeys = createTransportKeys();
Map<ContactId, TransportKeys> keys =
singletonMap(contactId, transportKeys);
Collection<KeySet> keys =
singletonList(new KeySet(keySetId, contactId, transportKeys));
context.checking(new Expectations() {{
// startTransaction()
oneOf(database).startTransaction();
will(returnValue(txn));
// updateTransportKeys()
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsTransport(txn, transportId);
will(returnValue(true));
oneOf(database).updateTransportKeys(txn, keys);
@@ -1337,22 +1347,22 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
private TransportKeys createTransportKeys() {
SecretKey inPrevTagKey = TestUtils.getSecretKey();
SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
SecretKey inPrevTagKey = getSecretKey();
SecretKey inPrevHeaderKey = getSecretKey();
IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
1, 123, new byte[4]);
SecretKey inCurrTagKey = TestUtils.getSecretKey();
SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
SecretKey inCurrTagKey = getSecretKey();
SecretKey inCurrHeaderKey = getSecretKey();
IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
2, 234, new byte[4]);
SecretKey inNextTagKey = TestUtils.getSecretKey();
SecretKey inNextHeaderKey = TestUtils.getSecretKey();
SecretKey inNextTagKey = getSecretKey();
SecretKey inNextHeaderKey = getSecretKey();
IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
3, 345, new byte[4]);
SecretKey outCurrTagKey = TestUtils.getSecretKey();
SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
SecretKey outCurrTagKey = getSecretKey();
SecretKey outCurrHeaderKey = getSecretKey();
OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
2, 456);
2, 456, true);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
}
@@ -1498,7 +1508,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@SuppressWarnings("unchecked")
public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345;
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
MessageId messageId2 = new MessageId(getRandomId());
context.checking(new Expectations() {{
// open()
oneOf(database).open(null);
@@ -1518,10 +1528,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// addMessageDependencies()
oneOf(database).containsMessage(txn, messageId);
will(returnValue(true));
oneOf(database).addMessageDependency(txn, groupId, messageId,
messageId1);
oneOf(database).addMessageDependency(txn, groupId, messageId,
messageId2);
oneOf(database).getMessageState(txn, messageId);
will(returnValue(DELIVERED));
oneOf(database).addMessageDependency(txn, message, messageId1,
DELIVERED);
oneOf(database).addMessageDependency(txn, message, messageId2,
DELIVERED);
// getMessageDependencies()
oneOf(database).containsMessage(txn, messageId);
will(returnValue(true));

View File

@@ -572,8 +572,9 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
messageMeta.get(g.getId()).add(mm);
db.mergeMessageMetadata(txn, m.getId(), mm);
if (k > 0) {
db.addMessageDependency(txn, g.getId(), m.getId(),
pickRandom(groupMessages.get(g.getId())));
MessageId dependency =
pickRandom(groupMessages.get(g.getId()));
db.addMessageDependency(txn, m, dependency, state);
}
groupMessages.get(g.getId()).add(m.getId());
}
@@ -598,8 +599,9 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
messageMeta.get(g.getId()).add(mm);
db.mergeMessageMetadata(txn, m.getId(), mm);
if (j > 0) {
db.addMessageDependency(txn, g.getId(), m.getId(),
pickRandom(groupMessages.get(g.getId())));
MessageId dependency =
pickRandom(groupMessages.get(g.getId()));
db.addMessageDependency(txn, m, dependency, DELIVERED);
}
groupMessages.get(g.getId()).add(m.getId());
}

View File

@@ -19,6 +19,8 @@ import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.system.SystemClock;
@@ -34,7 +36,6 @@ import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -42,23 +43,28 @@ import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -86,12 +92,12 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
private final Message message;
private final TransportId transportId;
private final ContactId contactId;
private final KeySetId keySetId, keySetId1;
JdbcDatabaseTest() throws Exception {
groupId = new GroupId(getRandomId());
clientId = new ClientId(getRandomString(123));
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
group = new Group(groupId, clientId, descriptor);
clientId = getClientId();
group = getGroup(clientId);
groupId = group.getId();
author = getAuthor();
localAuthor = getLocalAuthor();
messageId = new MessageId(getRandomId());
@@ -99,8 +105,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
size = 1234;
raw = getRandomBytes(size);
message = new Message(messageId, groupId, timestamp, raw);
transportId = new TransportId("id");
transportId = getTransportId();
contactId = new ContactId(1);
keySetId = new KeySetId(1);
keySetId1 = new KeySetId(2);
}
protected abstract JdbcDatabase createDatabase(DatabaseConfig config,
@@ -190,9 +198,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The contact has not seen the message, so it should be sendable
Collection<MessageId> ids =
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
// Changing the status to seen = true should make the message unsendable
db.raiseSeenFlag(txn, contactId, messageId);
@@ -228,9 +236,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Marking the message delivered should make it sendable
db.setMessageState(txn, messageId, DELIVERED);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
// Marking the message invalid should make it unsendable
db.setMessageState(txn, messageId, INVALID);
@@ -279,9 +287,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Sharing the group should make the message sendable
db.setGroupVisibility(txn, contactId, groupId, true);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
// Unsharing the group should make the message unsendable
db.setGroupVisibility(txn, contactId, groupId, false);
@@ -324,9 +332,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Sharing the message should make it sendable
db.setMessageShared(txn, messageId);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
db.commitTransaction(txn);
db.close();
@@ -352,7 +360,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The message is just the right size to send
ids = db.getMessagesToSend(txn, contactId, size);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
db.commitTransaction(txn);
db.close();
@@ -384,7 +392,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
// Both message IDs should have been removed
assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
assertEquals(emptyList(), db.getMessagesToAck(txn,
contactId, 1234));
// Raise the ack flag again
@@ -415,7 +423,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Retrieve the message from the database and mark it as sent
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
// The message should no longer be sendable
@@ -626,31 +634,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The group should not be visible to the contact
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.emptyMap(),
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
// Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false);
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonMap(contactId, false),
assertEquals(singletonMap(contactId, false),
db.getGroupVisibility(txn, groupId));
// Share the group with the contact
db.setGroupVisibility(txn, contactId, groupId, true);
assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonMap(contactId, true),
assertEquals(singletonMap(contactId, true),
db.getGroupVisibility(txn, groupId));
// Unshare the group with the contact
db.setGroupVisibility(txn, contactId, groupId, false);
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.singletonMap(contactId, false),
assertEquals(singletonMap(contactId, false),
db.getGroupVisibility(txn, groupId));
// Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(Collections.emptyMap(),
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
db.commitTransaction(txn);
@@ -660,48 +668,125 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
@Test
public void testTransportKeys() throws Exception {
TransportKeys keys = createTransportKeys();
TransportKeys keys1 = createTransportKeys();
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Initially there should be no transport keys in the database
assertEquals(Collections.emptyMap(),
db.getTransportKeys(txn, transportId));
assertEquals(emptyList(), db.getTransportKeys(txn, transportId));
// Add the contact, the transport and the transport keys
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.addTransportKeys(txn, contactId, keys);
assertEquals(keySetId, db.addTransportKeys(txn, contactId, keys));
assertEquals(keySetId1, db.addTransportKeys(txn, contactId, keys1));
// Retrieve the transport keys
Map<ContactId, TransportKeys> newKeys =
db.getTransportKeys(txn, transportId);
assertEquals(1, newKeys.size());
Entry<ContactId, TransportKeys> e =
newKeys.entrySet().iterator().next();
assertEquals(contactId, e.getKey());
TransportKeys k = e.getValue();
assertEquals(transportId, k.getTransportId());
assertKeysEquals(keys.getPreviousIncomingKeys(),
k.getPreviousIncomingKeys());
assertKeysEquals(keys.getCurrentIncomingKeys(),
k.getCurrentIncomingKeys());
assertKeysEquals(keys.getNextIncomingKeys(),
k.getNextIncomingKeys());
assertKeysEquals(keys.getCurrentOutgoingKeys(),
k.getCurrentOutgoingKeys());
Collection<KeySet> allKeys = db.getTransportKeys(txn, transportId);
assertEquals(2, allKeys.size());
for (KeySet ks : allKeys) {
assertEquals(contactId, ks.getContactId());
if (ks.getKeySetId().equals(keySetId)) {
assertKeysEquals(keys, ks.getTransportKeys());
} else {
assertEquals(keySetId1, ks.getKeySetId());
assertKeysEquals(keys1, ks.getTransportKeys());
}
}
// Removing the contact should remove the transport keys
db.removeContact(txn, contactId);
assertEquals(Collections.emptyMap(),
db.getTransportKeys(txn, transportId));
assertEquals(emptyList(), db.getTransportKeys(txn, transportId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testUnboundTransportKeys() throws Exception {
TransportKeys keys = createTransportKeys();
TransportKeys keys1 = createTransportKeys();
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Initially there should be no transport keys in the database
assertEquals(emptyList(), db.getTransportKeys(txn, transportId));
// Add the contact, the transport and the unbound transport keys
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
assertEquals(keySetId, db.addTransportKeys(txn, null, keys));
assertEquals(keySetId1, db.addTransportKeys(txn, null, keys1));
// Retrieve the transport keys
Collection<KeySet> allKeys = db.getTransportKeys(txn, transportId);
assertEquals(2, allKeys.size());
for (KeySet ks : allKeys) {
assertNull(ks.getContactId());
if (ks.getKeySetId().equals(keySetId)) {
assertKeysEquals(keys, ks.getTransportKeys());
} else {
assertEquals(keySetId1, ks.getKeySetId());
assertKeysEquals(keys1, ks.getTransportKeys());
}
}
// Bind the first set of transport keys
db.bindTransportKeys(txn, contactId, transportId, keySetId);
// Retrieve the keys again - the first set should be bound
allKeys = db.getTransportKeys(txn, transportId);
assertEquals(2, allKeys.size());
for (KeySet ks : allKeys) {
if (ks.getKeySetId().equals(keySetId)) {
assertEquals(contactId, ks.getContactId());
assertKeysEquals(keys, ks.getTransportKeys());
} else {
assertEquals(keySetId1, ks.getKeySetId());
assertNull(ks.getContactId());
assertKeysEquals(keys1, ks.getTransportKeys());
}
}
// Remove the unbound transport keys
db.removeTransportKeys(txn, transportId, keySetId1);
// Retrieve the keys again - the second set should be gone
allKeys = db.getTransportKeys(txn, transportId);
assertEquals(1, allKeys.size());
KeySet ks = allKeys.iterator().next();
assertEquals(keySetId, ks.getKeySetId());
assertEquals(contactId, ks.getContactId());
assertKeysEquals(keys, ks.getTransportKeys());
// Removing the transport should remove the remaining transport keys
db.removeTransport(txn, transportId);
assertEquals(emptyList(), db.getTransportKeys(txn, transportId));
db.commitTransaction(txn);
db.close();
}
private void assertKeysEquals(TransportKeys expected,
TransportKeys actual) {
assertEquals(expected.getTransportId(), actual.getTransportId());
assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
assertKeysEquals(expected.getPreviousIncomingKeys(),
actual.getPreviousIncomingKeys());
assertKeysEquals(expected.getCurrentIncomingKeys(),
actual.getCurrentIncomingKeys());
assertKeysEquals(expected.getNextIncomingKeys(),
actual.getNextIncomingKeys());
assertKeysEquals(expected.getCurrentOutgoingKeys(),
actual.getCurrentOutgoingKeys());
}
private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) {
assertArrayEquals(expected.getTagKey().getBytes(),
actual.getTagKey().getBytes());
@@ -719,6 +804,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
actual.getHeaderKey().getBytes());
assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
assertEquals(expected.getStreamCounter(), actual.getStreamCounter());
assertEquals(expected.isActive(), actual.isActive());
}
@Test
@@ -735,18 +821,18 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
db.updateTransportKeys(txn,
singletonList(new KeySet(keySetId, contactId, keys)));
// Increment the stream counter twice and retrieve the transport keys
db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
Map<ContactId, TransportKeys> newKeys =
db.getTransportKeys(txn, transportId);
db.incrementStreamCounter(txn, transportId, keySetId);
db.incrementStreamCounter(txn, transportId, keySetId);
Collection<KeySet> newKeys = db.getTransportKeys(txn, transportId);
assertEquals(1, newKeys.size());
Entry<ContactId, TransportKeys> e =
newKeys.entrySet().iterator().next();
assertEquals(contactId, e.getKey());
TransportKeys k = e.getValue();
KeySet ks = newKeys.iterator().next();
assertEquals(keySetId, ks.getKeySetId());
assertEquals(contactId, ks.getContactId());
TransportKeys k = ks.getTransportKeys();
assertEquals(transportId, k.getTransportId());
OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
assertEquals(rotationPeriod, outCurr.getRotationPeriod());
@@ -771,19 +857,19 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addTransport(txn, transportId, 123);
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
db.updateTransportKeys(txn,
singletonList(new KeySet(keySetId, contactId, keys)));
// Update the reordering window and retrieve the transport keys
new Random().nextBytes(bitmap);
db.setReorderingWindow(txn, contactId, transportId, rotationPeriod,
db.setReorderingWindow(txn, keySetId, transportId, rotationPeriod,
base + 1, bitmap);
Map<ContactId, TransportKeys> newKeys =
db.getTransportKeys(txn, transportId);
Collection<KeySet> newKeys = db.getTransportKeys(txn, transportId);
assertEquals(1, newKeys.size());
Entry<ContactId, TransportKeys> e =
newKeys.entrySet().iterator().next();
assertEquals(contactId, e.getKey());
TransportKeys k = e.getValue();
KeySet ks = newKeys.iterator().next();
assertEquals(keySetId, ks.getKeySetId());
assertEquals(contactId, ks.getContactId());
TransportKeys k = ks.getTransportKeys();
assertEquals(transportId, k.getTransportId());
IncomingKeys inCurr = k.getCurrentIncomingKeys();
assertEquals(rotationPeriod, inCurr.getRotationPeriod());
@@ -830,18 +916,18 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addLocalAuthor(txn, localAuthor);
Collection<ContactId> contacts =
db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.emptyList(), contacts);
assertEquals(emptyList(), contacts);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
contacts = db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.singletonList(contactId), contacts);
assertEquals(singletonList(contactId), contacts);
// Remove the local author - the contact should be removed
db.removeLocalAuthor(txn, localAuthor.getId());
contacts = db.getContacts(txn, localAuthor.getId());
assertEquals(Collections.emptyList(), contacts);
assertEquals(emptyList(), contacts);
assertFalse(db.containsContact(txn, contactId));
db.commitTransaction(txn);
@@ -1227,6 +1313,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
MessageId messageId4 = new MessageId(getRandomId());
Message message1 = new Message(messageId1, groupId, timestamp, raw);
Message message2 = new Message(messageId2, groupId, timestamp, raw);
Message message3 = new Message(messageId3, groupId, timestamp, raw);
Message message4 = new Message(messageId4, groupId, timestamp, raw);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
@@ -1234,21 +1322,21 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and some messages
db.addGroup(txn, group);
db.addMessage(txn, message, PENDING, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, contactId);
db.addMessage(txn, message1, PENDING, true, contactId);
db.addMessage(txn, message2, INVALID, true, contactId);
// Add dependencies
db.addMessageDependency(txn, groupId, messageId, messageId1);
db.addMessageDependency(txn, groupId, messageId, messageId2);
db.addMessageDependency(txn, groupId, messageId1, messageId3);
db.addMessageDependency(txn, groupId, messageId2, messageId4);
db.addMessageDependency(txn, message, messageId1, PENDING);
db.addMessageDependency(txn, message, messageId2, PENDING);
db.addMessageDependency(txn, message1, messageId3, PENDING);
db.addMessageDependency(txn, message2, messageId4, INVALID);
Map<MessageId, State> dependencies;
// Retrieve dependencies for root
dependencies = db.getMessageDependencies(txn, messageId);
assertEquals(2, dependencies.size());
assertEquals(DELIVERED, dependencies.get(messageId1));
assertEquals(PENDING, dependencies.get(messageId1));
assertEquals(INVALID, dependencies.get(messageId2));
// Retrieve dependencies for message 1
@@ -1281,10 +1369,24 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(1, dependents.size());
assertEquals(PENDING, dependents.get(messageId));
// Message 3 is missing, so it has no dependents
dependents = db.getMessageDependents(txn, messageId3);
assertEquals(0, dependents.size());
// Add message 3
db.addMessage(txn, message3, UNKNOWN, false, contactId);
// Message 3 has message 1 as a dependent
dependents = db.getMessageDependents(txn, messageId3);
assertEquals(1, dependents.size());
assertEquals(DELIVERED, dependents.get(messageId1));
assertEquals(PENDING, dependents.get(messageId1));
// Message 4 is missing, so it has no dependents
dependents = db.getMessageDependents(txn, messageId4);
assertEquals(0, dependents.size());
// Add message 4
db.addMessage(txn, message4, UNKNOWN, false, contactId);
// Message 4 has message 2 as a dependent
dependents = db.getMessageDependents(txn, messageId4);
@@ -1305,9 +1407,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addMessage(txn, message, PENDING, true, contactId);
// Add a second group
GroupId groupId1 = new GroupId(getRandomId());
Group group1 = new Group(groupId1, clientId,
getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
Group group1 = getGroup(clientId);
GroupId groupId1 = group1.getId();
db.addGroup(txn, group1);
// Add a message to the second group
@@ -1324,16 +1425,16 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addMessage(txn, message3, DELIVERED, true, contactId);
// Add dependencies between the messages
db.addMessageDependency(txn, groupId, messageId, messageId1);
db.addMessageDependency(txn, groupId, messageId, messageId2);
db.addMessageDependency(txn, groupId, messageId, messageId3);
db.addMessageDependency(txn, message, messageId1, PENDING);
db.addMessageDependency(txn, message, messageId2, PENDING);
db.addMessageDependency(txn, message, messageId3, PENDING);
// Retrieve the dependencies for the root
Map<MessageId, State> dependencies;
dependencies = db.getMessageDependencies(txn, messageId);
// The cross-group dependency should have state INVALID
assertEquals(INVALID, dependencies.get(messageId1));
// The cross-group dependency should have state UNKNOWN
assertEquals(UNKNOWN, dependencies.get(messageId1));
// The missing dependency should have state UNKNOWN
assertEquals(UNKNOWN, dependencies.get(messageId2));
@@ -1345,8 +1446,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
Map<MessageId, State> dependents;
dependents = db.getMessageDependents(txn, messageId1);
// The cross-group dependent should have its real state
assertEquals(PENDING, dependents.get(messageId));
// The cross-group dependent should be excluded
assertFalse(dependents.containsKey(messageId));
db.commitTransaction(txn);
db.close();
@@ -1411,9 +1512,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addMessage(txn, m4, DELIVERED, true, contactId);
// Introduce dependencies between the messages
db.addMessageDependency(txn, groupId, mId1, mId2);
db.addMessageDependency(txn, groupId, mId3, mId1);
db.addMessageDependency(txn, groupId, mId4, mId3);
db.addMessageDependency(txn, m1, mId2, DELIVERED);
db.addMessageDependency(txn, m3, mId1, DELIVERED);
db.addMessageDependency(txn, m4, mId3, DELIVERED);
// Retrieve messages to be shared
Collection<MessageId> result = db.getMessagesToShare(txn);
@@ -1544,9 +1645,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The message should be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100);
assertEquals(Collections.singletonList(messageId), ids);
assertEquals(singletonList(messageId), ids);
// The raw message should not be null
assertNotNull(db.getRawMessage(txn, messageId));
@@ -1719,7 +1820,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
SecretKey outCurrTagKey = getSecretKey();
SecretKey outCurrHeaderKey = getSecretKey();
OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
2, 456);
2, 456, true);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
}

View File

@@ -19,6 +19,7 @@ import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.RE
import static org.briarproject.bramble.api.keyagreement.RecordTypes.ABORT;
import static org.briarproject.bramble.api.keyagreement.RecordTypes.CONFIRM;
import static org.briarproject.bramble.api.keyagreement.RecordTypes.KEY;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -31,7 +32,7 @@ public class KeyAgreementTransportTest extends BrambleMockTestCase {
private final TransportConnectionWriter transportConnectionWriter =
context.mock(TransportConnectionWriter.class);
private final TransportId transportId = new TransportId("test");
private final TransportId transportId = getTransportId();
private final KeyAgreementConnection keyAgreementConnection =
new KeyAgreementConnection(duplexTransportConnection, transportId);

View File

@@ -17,6 +17,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.NoSuchElementException;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -29,8 +30,8 @@ public class ConnectionRegistryImplTest extends BrambleTestCase {
public ConnectionRegistryImplTest() {
contactId = new ContactId(1);
contactId1 = new ContactId(2);
transportId = new TransportId("id");
transportId1 = new TransportId("id1");
transportId = getTransportId();
transportId1 = getTransportId();
}
@Test

View File

@@ -24,6 +24,8 @@ import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
public class PluginManagerImplTest extends BrambleTestCase {
@Test
@@ -46,21 +48,21 @@ public class PluginManagerImplTest extends BrambleTestCase {
SimplexPluginFactory simplexFactory =
context.mock(SimplexPluginFactory.class);
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
TransportId simplexId = new TransportId("simplex");
TransportId simplexId = getTransportId();
SimplexPluginFactory simplexFailFactory =
context.mock(SimplexPluginFactory.class, "simplexFailFactory");
SimplexPlugin simplexFailPlugin =
context.mock(SimplexPlugin.class, "simplexFailPlugin");
TransportId simplexFailId = new TransportId("simplex1");
TransportId simplexFailId = getTransportId();
// Two duplex plugin factories: one creates a plugin, the other fails
DuplexPluginFactory duplexFactory =
context.mock(DuplexPluginFactory.class);
DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class);
TransportId duplexId = new TransportId("duplex");
TransportId duplexId = getTransportId();
DuplexPluginFactory duplexFailFactory =
context.mock(DuplexPluginFactory.class, "duplexFailFactory");
TransportId duplexFailId = new TransportId("duplex1");
TransportId duplexFailId = getTransportId();
context.checking(new Expectations() {{
allowing(simplexPlugin).getId();

View File

@@ -32,6 +32,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
public class PollerTest extends BrambleMockTestCase {
@@ -48,7 +49,7 @@ public class PollerTest extends BrambleMockTestCase {
private final SecureRandom random;
private final Executor ioExecutor = new ImmediateExecutor();
private final TransportId transportId = new TransportId("id");
private final TransportId transportId = getTransportId();
private final ContactId contactId = new ContactId(234);
private final int pollingInterval = 60 * 1000;
private final long now = System.currentTimeMillis();
@@ -64,7 +65,7 @@ public class PollerTest extends BrambleMockTestCase {
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
SimplexPlugin simplexPlugin1 =
context.mock(SimplexPlugin.class, "simplexPlugin1");
TransportId simplexId1 = new TransportId("simplex1");
TransportId simplexId1 = getTransportId();
List<SimplexPlugin> simplexPlugins = Arrays.asList(simplexPlugin,
simplexPlugin1);
TransportConnectionWriter simplexWriter =
@@ -72,7 +73,7 @@ public class PollerTest extends BrambleMockTestCase {
// Two duplex plugins: one supports polling, the other doesn't
DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class);
TransportId duplexId = new TransportId("duplex");
TransportId duplexId = getTransportId();
DuplexPlugin duplexPlugin1 =
context.mock(DuplexPlugin.class, "duplexPlugin1");
List<DuplexPlugin> duplexPlugins = Arrays.asList(duplexPlugin,
@@ -349,7 +350,6 @@ public class PollerTest extends BrambleMockTestCase {
@Test
public void testCancelsPollingOnTransportDisabled() throws Exception {
Plugin plugin = context.mock(Plugin.class);
List<ContactId> connected = Collections.singletonList(contactId);
context.checking(new Expectations() {{
allowing(plugin).getId();

View File

@@ -32,9 +32,9 @@ import java.util.Map;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_VERSION;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
@@ -51,7 +51,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
context.mock(ContactGroupFactory.class);
private final Clock clock = context.mock(Clock.class);
private final Group localGroup = getGroup();
private final Group localGroup = getGroup(CLIENT_ID);
private final LocalAuthor localAuthor = getLocalAuthor();
private final BdfDictionary fooPropertiesDict = BdfDictionary.of(
new BdfEntry("fooKey1", "fooValue1"),
@@ -90,7 +90,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact1 = getContact(true);
Contact contact2 = getContact(true);
List<Contact> contacts = Arrays.asList(contact1, contact2);
Group contactGroup1 = getGroup(), contactGroup2 = getGroup();
Group contactGroup1 = getGroup(CLIENT_ID);
Group contactGroup2 = getGroup(CLIENT_ID);
context.checking(new Expectations() {{
oneOf(db).containsGroup(txn, localGroup.getId());
@@ -143,7 +144,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testCreatesContactGroupWhenAddingContact() throws Exception {
Transaction txn = new Transaction(null, false);
Contact contact = getContact(true);
Group contactGroup = getGroup();
Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{
// Create the group and share it with the contact
@@ -171,7 +172,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testRemovesGroupWhenRemovingContact() throws Exception {
Transaction txn = new Transaction(null, false);
Contact contact = getContact(true);
Group contactGroup = getGroup();
Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
@@ -306,7 +307,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
@Test
public void testStoresRemotePropertiesWithVersion0() throws Exception {
Contact contact = getContact(true);
Group contactGroup = getGroup();
Group contactGroup = getGroup(CLIENT_ID);
Transaction txn = new Transaction(null, false);
Map<TransportId, TransportProperties> properties =
new LinkedHashMap<>();
@@ -399,6 +400,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
}});
@@ -417,8 +421,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact3 = getContact(true);
List<Contact> contacts =
Arrays.asList(contact1, contact2, contact3);
Group contactGroup2 = getGroup();
Group contactGroup3 = getGroup();
Group contactGroup2 = getGroup(CLIENT_ID);
Group contactGroup3 = getGroup(CLIENT_ID);
Map<MessageId, BdfDictionary> messageMetadata3 =
new LinkedHashMap<>();
// A remote update for another transport should be ignored
@@ -466,6 +470,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata3));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
}});
@@ -501,6 +508,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
// Properties are unchanged so we're done
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
@@ -514,7 +524,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testMergingNewPropertiesCreatesUpdate() throws Exception {
Transaction txn = new Transaction(null, false);
Contact contact = getContact(true);
Group contactGroup = getGroup();
Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{
oneOf(db).startTransaction(false);
@@ -549,7 +559,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testMergingUpdatedPropertiesCreatesUpdate() throws Exception {
Transaction txn = new Transaction(null, false);
Contact contact = getContact(true);
Group contactGroup = getGroup();
Group contactGroup = getGroup(CLIENT_ID);
BdfDictionary oldMetadata = BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
@@ -561,9 +571,12 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
MessageId contactGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> contactGroupMessageMetadata =
Collections.singletonMap(contactGroupUpdateId, oldMetadata);
BdfList oldUpdate = BdfList.of("foo", 1, BdfDictionary.of(
TransportProperties oldProperties = new TransportProperties();
oldProperties.put("fooKey1", "oldFooValue1");
BdfDictionary oldPropertiesDict = BdfDictionary.of(
new BdfEntry("fooKey1", "oldFooValue1")
));
);
BdfList oldUpdate = BdfList.of("foo", 1, oldPropertiesDict);
context.checking(new Expectations() {{
oneOf(db).startTransaction(false);
@@ -574,6 +587,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(localGroupMessageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localGroupUpdateId);
will(returnValue(oldUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
oldPropertiesDict);
will(returnValue(oldProperties));
// Store the merged properties in the local group, version 2
expectStoreMessage(txn, localGroup.getId(), "foo",
fooPropertiesDict, 2, true, false);
@@ -600,12 +616,6 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
t.mergeLocalProperties(new TransportId("foo"), fooProperties);
}
private Group getGroup() {
GroupId g = new GroupId(getRandomId());
byte[] descriptor = getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH);
return new Group(g, CLIENT_ID, descriptor);
}
private Contact getContact(boolean active) {
ContactId c = new ContactId(nextContactId++);
return new Contact(c, getAuthor(), localAuthor.getId(),
@@ -643,8 +653,14 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// Retrieve and parse the latest local properties
oneOf(clientHelper).getMessageAsList(txn, fooVersion999);
will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(clientHelper).getMessageAsList(txn, barVersion3);
will(returnValue(barUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
barPropertiesDict);
will(returnValue(barProperties));
}});
}

View File

@@ -3,80 +3,78 @@ package org.briarproject.bramble.properties;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Mockery;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test;
import java.io.IOException;
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertEquals;
public class TransportPropertyValidatorTest extends BrambleTestCase {
public class TransportPropertyValidatorTest extends BrambleMockTestCase {
private final ClientHelper clientHelper = context.mock(ClientHelper.class);
private final TransportId transportId;
private final BdfDictionary bdfDictionary;
private final TransportProperties transportProperties;
private final Group group;
private final Message message;
private final TransportPropertyValidator tpv;
public TransportPropertyValidatorTest() {
transportId = new TransportId("test");
bdfDictionary = new BdfDictionary();
transportId = getTransportId();
bdfDictionary = BdfDictionary.of(new BdfEntry("foo", "bar"));
transportProperties = new TransportProperties();
transportProperties.put("foo", "bar");
GroupId groupId = new GroupId(TestUtils.getRandomId());
ClientId clientId = new ClientId(StringUtils.getRandomString(5));
byte[] descriptor = TestUtils.getRandomBytes(12);
group = new Group(groupId, clientId, descriptor);
group = getGroup(getClientId());
message = getMessage(group.getId());
MessageId messageId = new MessageId(TestUtils.getRandomId());
long timestamp = System.currentTimeMillis();
byte[] body = TestUtils.getRandomBytes(123);
message = new Message(messageId, groupId, timestamp, body);
Mockery context = new Mockery();
ClientHelper clientHelper = context.mock(ClientHelper.class);
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
Clock clock = context.mock(Clock.class);
tpv = new TransportPropertyValidator(clientHelper, metadataEncoder,
clock);
}
@Test
public void testValidateProperMessage() throws IOException {
BdfList body = BdfList.of(transportId.getString(), 4, bdfDictionary);
BdfDictionary result = tpv.validateMessage(message, group, body)
.getDictionary();
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateTransportProperties(
bdfDictionary);
will(returnValue(transportProperties));
}});
assertEquals("test", result.getString("transportId"));
BdfDictionary result =
tpv.validateMessage(message, group, body).getDictionary();
assertEquals(transportId.getString(), result.getString("transportId"));
assertEquals(4, result.getLong("version").longValue());
}
@Test(expected = FormatException.class)
public void testValidateWrongVersionValue() throws IOException {
BdfList body = BdfList.of(transportId.getString(), -1, bdfDictionary);
tpv.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateWrongVersionType() throws IOException {
BdfList body = BdfList.of(transportId.getString(), bdfDictionary,
bdfDictionary);
tpv.validateMessage(message, group, body);
@@ -84,27 +82,15 @@ public class TransportPropertyValidatorTest extends BrambleTestCase {
@Test(expected = FormatException.class)
public void testValidateLongTransportId() throws IOException {
String wrongTransportIdString =
StringUtils.getRandomString(MAX_TRANSPORT_ID_LENGTH + 1);
getRandomString(MAX_TRANSPORT_ID_LENGTH + 1);
BdfList body = BdfList.of(wrongTransportIdString, 4, bdfDictionary);
tpv.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateEmptyTransportId() throws IOException {
BdfList body = BdfList.of("", 4, bdfDictionary);
tpv.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateTooManyProperties() throws IOException {
BdfDictionary d = new BdfDictionary();
for (int i = 0; i < MAX_PROPERTIES_PER_TRANSPORT + 1; i++)
d.put(String.valueOf(i), i);
BdfList body = BdfList.of(transportId.getString(), 4, d);
tpv.validateMessage(message, group, body);
}
}

View File

@@ -36,7 +36,8 @@ import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.transport.TransportConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -73,13 +74,13 @@ public class SyncIntegrationTest extends BrambleTestCase {
component.inject(this);
contactId = new ContactId(234);
transportId = new TransportId("id");
transportId = getTransportId();
// Create the transport keys
tagKey = TestUtils.getSecretKey();
headerKey = TestUtils.getSecretKey();
streamNumber = 123;
// Create a group
ClientId clientId = new ClientId(getRandomString(123));
ClientId clientId = getClientId();
int clientVersion = 1234567890;
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
Group group = groupFactory.createGroup(clientId, clientVersion,

View File

@@ -21,9 +21,7 @@ import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.ByteUtils;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test;
@@ -38,6 +36,9 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERE
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
public class ValidationManagerImplTest extends BrambleMockTestCase {
@@ -51,14 +52,12 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
private final Executor dbExecutor = new ImmediateExecutor();
private final Executor validationExecutor = new ImmediateExecutor();
private final ClientId clientId =
new ClientId(StringUtils.getRandomString(5));
private final MessageId messageId = new MessageId(TestUtils.getRandomId());
private final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
private final MessageId messageId2 = new MessageId(TestUtils.getRandomId());
private final GroupId groupId = new GroupId(TestUtils.getRandomId());
private final byte[] descriptor = new byte[32];
private final Group group = new Group(groupId, clientId, descriptor);
private final ClientId clientId = getClientId();
private final MessageId messageId = new MessageId(getRandomId());
private final MessageId messageId1 = new MessageId(getRandomId());
private final MessageId messageId2 = new MessageId(getRandomId());
private final Group group = getGroup(clientId);
private final GroupId groupId = group.getId();
private final long timestamp = System.currentTimeMillis();
private final byte[] raw = new byte[123];
private final Message message = new Message(messageId, groupId, timestamp,
@@ -716,8 +715,8 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test
public void testRecursiveInvalidation() throws Exception {
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
MessageId messageId4 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
MessageId messageId4 = new MessageId(getRandomId());
Map<MessageId, State> twoDependents = new LinkedHashMap<>();
twoDependents.put(messageId1, PENDING);
twoDependents.put(messageId2, PENDING);
@@ -819,8 +818,8 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test
public void testPendingDependentsGetDelivered() throws Exception {
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
MessageId messageId4 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
MessageId messageId4 = new MessageId(getRandomId());
Message message3 = new Message(messageId3, groupId, timestamp,
raw);
Message message4 = new Message(messageId4, groupId, timestamp,

View File

@@ -16,10 +16,12 @@ import javax.annotation.Nullable;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@Module
public class TestPluginConfigModule {
public static final TransportId TRANSPORT_ID = new TransportId("id");
public static final TransportId TRANSPORT_ID = getTransportId();
public static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
@NotNullByDefault

View File

@@ -1,18 +1,20 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.jmock.Expectations;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage;
public abstract class ValidatorTestCase extends BrambleMockTestCase {
@@ -21,17 +23,27 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase {
protected final MetadataEncoder metadataEncoder =
context.mock(MetadataEncoder.class);
protected final Clock clock = context.mock(Clock.class);
protected final AuthorFactory authorFactory =
context.mock(AuthorFactory.class);
protected final MessageId messageId = new MessageId(getRandomId());
protected final GroupId groupId = new GroupId(getRandomId());
protected final long timestamp = 1234567890 * 1000L;
protected final byte[] raw = getRandomBytes(123);
protected final Message message =
new Message(messageId, groupId, timestamp, raw);
protected final ClientId clientId = new ClientId(getRandomString(123));
protected final byte[] descriptor = getRandomBytes(123);
protected final Group group = new Group(groupId, clientId, descriptor);
protected final Group group = getGroup(getClientId());
protected final GroupId groupId = group.getId();
protected final byte[] descriptor = group.getDescriptor();
protected final Message message = getMessage(groupId);
protected final MessageId messageId = message.getId();
protected final long timestamp = message.getTimestamp();
protected final byte[] raw = message.getRaw();
protected final Author author = getAuthor();
protected final BdfList authorList = BdfList.of(
author.getFormatVersion(),
author.getName(),
author.getPublicKey()
);
}
protected void expectParseAuthor(BdfList authorList, Author author)
throws Exception {
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList);
will(returnValue(author));
}});
}
}

View File

@@ -12,51 +12,51 @@ import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.concurrent.DeterministicExecutor;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Random;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.junit.Assert.assertEquals;
public class KeyManagerImplTest extends BrambleTestCase {
public class KeyManagerImplTest extends BrambleMockTestCase {
private final Mockery context = new Mockery();
private final KeyManagerImpl keyManager;
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
private final PluginConfig pluginConfig = context.mock(PluginConfig.class);
private final TransportKeyManagerFactory transportKeyManagerFactory =
context.mock(TransportKeyManagerFactory.class);
private final TransportKeyManager transportKeyManager =
context.mock(TransportKeyManager.class);
private final DeterministicExecutor executor = new DeterministicExecutor();
private final Transaction txn = new Transaction(null, false);
private final ContactId contactId = new ContactId(42);
private final ContactId inactiveContactId = new ContactId(43);
private final TransportId transportId = new TransportId("tId");
private final TransportId unknownTransportId = new TransportId("id");
private final ContactId contactId = new ContactId(123);
private final ContactId inactiveContactId = new ContactId(234);
private final KeySetId keySetId = new KeySetId(345);
private final TransportId transportId = getTransportId();
private final TransportId unknownTransportId = getTransportId();
private final StreamContext streamContext =
new StreamContext(contactId, transportId, getSecretKey(),
getSecretKey(), 1);
private final byte[] tag = getRandomBytes(TAG_LENGTH);
public KeyManagerImplTest() {
keyManager = new KeyManagerImpl(db, executor, pluginConfig,
transportKeyManagerFactory);
}
private final KeyManagerImpl keyManager = new KeyManagerImpl(db, executor,
pluginConfig, transportKeyManagerFactory);
@Before
public void testStartService() throws Exception {
@@ -70,8 +70,8 @@ public class KeyManagerImplTest extends BrambleTestCase {
true, false));
SimplexPluginFactory pluginFactory =
context.mock(SimplexPluginFactory.class);
Collection<SimplexPluginFactory> factories = Collections
.singletonList(pluginFactory);
Collection<SimplexPluginFactory> factories =
singletonList(pluginFactory);
int maxLatency = 1337;
context.checking(new Expectations() {{
@@ -110,7 +110,22 @@ public class KeyManagerImplTest extends BrambleTestCase {
}});
keyManager.addContact(txn, contactId, secretKey, timestamp, alice);
context.assertIsSatisfied();
}
@Test
public void testAddUnboundKeys() throws Exception {
SecretKey secretKey = getSecretKey();
long timestamp = System.currentTimeMillis();
boolean alice = new Random().nextBoolean();
context.checking(new Expectations() {{
oneOf(transportKeyManager).addUnboundKeys(txn, secretKey,
timestamp, alice);
will(returnValue(keySetId));
}});
assertEquals(singletonMap(transportId, keySetId),
keyManager.addUnboundKeys(txn, secretKey, timestamp, alice));
}
@Test
@@ -138,7 +153,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
assertEquals(streamContext,
keyManager.getStreamContext(contactId, transportId));
context.assertIsSatisfied();
}
@Test
@@ -161,7 +175,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
assertEquals(streamContext,
keyManager.getStreamContext(transportId, tag));
context.assertIsSatisfied();
}
@Test
@@ -175,8 +188,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
keyManager.eventOccurred(event);
executor.runUntilIdle();
assertEquals(null, keyManager.getStreamContext(contactId, transportId));
context.assertIsSatisfied();
}
@Test
@@ -196,8 +207,5 @@ public class KeyManagerImplTest extends BrambleTestCase {
keyManager.eventOccurred(event);
assertEquals(streamContext,
keyManager.getStreamContext(inactiveContactId, transportId));
context.assertIsSatisfied();
}
}

View File

@@ -8,6 +8,8 @@ import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.KeySet;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.TransportKeys;
@@ -22,23 +24,25 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.bramble.api.transport.TransportConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
import static org.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@@ -50,11 +54,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
context.mock(ScheduledExecutorService.class);
private final Clock clock = context.mock(Clock.class);
private final TransportId transportId = new TransportId("id");
private final TransportId transportId = getTransportId();
private final long maxLatency = 30 * 1000; // 30 seconds
private final long rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
private final ContactId contactId = new ContactId(123);
private final ContactId contactId1 = new ContactId(234);
private final KeySetId keySetId = new KeySetId(345);
private final KeySetId keySetId1 = new KeySetId(456);
private final KeySetId keySetId2 = new KeySetId(567);
private final SecretKey tagKey = TestUtils.getSecretKey();
private final SecretKey headerKey = TestUtils.getSecretKey();
private final SecretKey masterKey = TestUtils.getSecretKey();
@@ -62,12 +69,16 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test
public void testKeysAreRotatedAtStartup() throws Exception {
Map<ContactId, TransportKeys> loaded = new LinkedHashMap<>();
TransportKeys shouldRotate = createTransportKeys(900, 0);
TransportKeys shouldNotRotate = createTransportKeys(1000, 0);
loaded.put(contactId, shouldRotate);
loaded.put(contactId1, shouldNotRotate);
TransportKeys rotated = createTransportKeys(1000, 0);
TransportKeys shouldRotate = createTransportKeys(900, 0, true);
TransportKeys shouldNotRotate = createTransportKeys(1000, 0, true);
TransportKeys shouldRotate1 = createTransportKeys(999, 0, false);
Collection<KeySet> loaded = asList(
new KeySet(keySetId, contactId, shouldRotate),
new KeySet(keySetId1, contactId1, shouldNotRotate),
new KeySet(keySetId2, null, shouldRotate1)
);
TransportKeys rotated = createTransportKeys(1000, 0, true);
TransportKeys rotated1 = createTransportKeys(1000, 0, false);
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
@@ -82,6 +93,8 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(returnValue(rotated));
oneOf(transportCrypto).rotateTransportKeys(shouldNotRotate, 1000);
will(returnValue(shouldNotRotate));
oneOf(transportCrypto).rotateTransportKeys(shouldRotate1, 1000);
will(returnValue(rotated1));
// Encode the tags (3 sets per contact)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(6).of(transportCrypto).encodeTag(
@@ -90,8 +103,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(new EncodeTagAction());
}
// Save the keys that were rotated
oneOf(db).updateTransportKeys(txn,
Collections.singletonMap(contactId, rotated));
oneOf(db).updateTransportKeys(txn, asList(
new KeySet(keySetId, contactId, rotated),
new KeySet(keySetId2, null, rotated1))
);
// Schedule key rotation at the start of the next rotation period
oneOf(scheduler).schedule(with(any(Runnable.class)),
with(rotationPeriodLength - 1), with(MILLISECONDS));
@@ -101,18 +116,19 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
transportKeyManager.start(txn);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
}
@Test
public void testKeysAreRotatedWhenAddingContact() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(999, 0);
TransportKeys rotated = createTransportKeys(1000, 0);
TransportKeys transportKeys = createTransportKeys(999, 0, true);
TransportKeys rotated = createTransportKeys(1000, 0, true);
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
999, alice);
999, alice, true);
will(returnValue(transportKeys));
// Get the current time (1 ms after start of rotation period 1000)
oneOf(clock).currentTimeMillis();
@@ -129,6 +145,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
}
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, rotated);
will(returnValue(keySetId));
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
@@ -138,6 +155,39 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000 - 1;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
}
@Test
public void testKeysAreRotatedWhenAddingUnboundKeys() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(999, 0, false);
TransportKeys rotated = createTransportKeys(1000, 0, false);
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
999, alice, false);
will(returnValue(transportKeys));
// Get the current time (1 ms after start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000 + 1));
// Rotate the transport keys
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(rotated));
// Save the keys
oneOf(db).addTransportKeys(txn, null, rotated);
will(returnValue(keySetId));
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
// The timestamp is 1 ms before the start of rotation period 1000
long timestamp = rotationPeriodLength * 1000 - 1;
assertEquals(keySetId, transportKeyManager.addUnboundKeys(txn,
masterKey, timestamp, alice));
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
}
@Test
@@ -149,6 +199,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
assertNull(transportKeyManager.getStreamContext(txn, contactId));
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
}
@Test
@@ -157,29 +208,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
boolean alice = random.nextBoolean();
// The stream counter has been exhausted
TransportKeys transportKeys = createTransportKeys(1000,
MAX_32_BIT_UNSIGNED + 1);
MAX_32_BIT_UNSIGNED + 1, true);
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000));
// Encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction());
}
// Rotate the transport keys (the keys are unaffected)
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(transportKeys));
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
}});
expectAddContactNoRotation(alice, transportKeys, txn);
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
@@ -188,6 +220,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice);
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId));
}
@@ -196,30 +229,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
boolean alice = random.nextBoolean();
// The stream counter can be used one more time before being exhausted
TransportKeys transportKeys = createTransportKeys(1000,
MAX_32_BIT_UNSIGNED);
MAX_32_BIT_UNSIGNED, true);
Transaction txn = new Transaction(null, false);
expectAddContactNoRotation(alice, transportKeys, txn);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000));
// Encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction());
}
// Rotate the transport keys (the keys are unaffected)
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(transportKeys));
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
// Increment the stream counter
oneOf(db).incrementStreamCounter(txn, contactId, transportId, 1000);
oneOf(db).incrementStreamCounter(txn, transportId, keySetId);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
@@ -230,6 +247,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice);
// The first request should return a stream context
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
StreamContext ctx = transportKeyManager.getStreamContext(txn,
contactId);
assertNotNull(ctx);
@@ -239,6 +257,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
assertEquals(headerKey, ctx.getHeaderKey());
assertEquals(MAX_32_BIT_UNSIGNED, ctx.getStreamNumber());
// The second request should return null, the counter is exhausted
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId));
}
@@ -246,29 +265,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
public void testIncomingStreamContextIsNullIfTagIsNotFound()
throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0);
TransportKeys transportKeys = createTransportKeys(1000, 0, true);
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000));
// Encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction());
}
// Rotate the transport keys (the keys are unaffected)
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(transportKeys));
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
}});
expectAddContactNoRotation(alice, transportKeys, txn);
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
@@ -277,6 +277,8 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
// The tag should not be recognised
assertNull(transportKeyManager.getStreamContext(txn,
new byte[TAG_LENGTH]));
}
@@ -284,14 +286,15 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test
public void testTagIsNotRecognisedTwice() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0);
TransportKeys transportKeys = createTransportKeys(1000, 0, true);
Transaction txn = new Transaction(null, false);
// Keep a copy of the tags
List<byte[]> tags = new ArrayList<>();
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice);
1000, alice, true);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
@@ -308,13 +311,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(returnValue(transportKeys));
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
will(returnValue(keySetId));
// Encode a new tag after sliding the window
oneOf(transportCrypto).encodeTag(with(any(byte[].class)),
with(tagKey), with(PROTOCOL_VERSION),
with((long) REORDERING_WINDOW_SIZE));
will(new EncodeTagAction(tags));
// Save the reordering window (previous rotation period, base 1)
oneOf(db).setReorderingWindow(txn, contactId, transportId, 999,
oneOf(db).setReorderingWindow(txn, keySetId, transportId, 999,
1, new byte[REORDERING_WINDOW_SIZE / 8]);
}});
@@ -325,6 +329,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
// Use the first tag (previous rotation period, stream number 0)
assertEquals(REORDERING_WINDOW_SIZE * 3, tags.size());
byte[] tag = tags.get(0);
@@ -344,10 +349,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test
public void testKeysAreRotatedToCurrentPeriod() throws Exception {
TransportKeys transportKeys = createTransportKeys(1000, 0);
Map<ContactId, TransportKeys> loaded =
Collections.singletonMap(contactId, transportKeys);
TransportKeys rotated = createTransportKeys(1001, 0);
TransportKeys transportKeys = createTransportKeys(1000, 0, true);
Collection<KeySet> loaded =
singletonList(new KeySet(keySetId, contactId, transportKeys));
TransportKeys rotated = createTransportKeys(1001, 0, true);
Transaction txn = new Transaction(null, false);
Transaction txn1 = new Transaction(null, false);
@@ -393,7 +398,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
}
// Save the keys that were rotated
oneOf(db).updateTransportKeys(txn1,
Collections.singletonMap(contactId, rotated));
singletonList(new KeySet(keySetId, contactId, rotated)));
// Schedule key rotation at the start of the next rotation period
oneOf(scheduler).schedule(with(any(Runnable.class)),
with(rotationPeriodLength), with(MILLISECONDS));
@@ -406,10 +411,197 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
transportKeyManager.start(txn);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
}
@Test
public void testBindingAndActivatingKeys() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0, false);
Transaction txn = new Transaction(null, false);
expectAddUnboundKeysNoRotation(alice, transportKeys, txn);
context.checking(new Expectations() {{
// When the keys are bound, encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction());
}
// Save the key binding
oneOf(db).bindTransportKeys(txn, contactId, transportId, keySetId);
// Activate the keys
oneOf(db).setTransportKeysActive(txn, transportId, keySetId);
// Increment the stream counter
oneOf(db).incrementStreamCounter(txn, transportId, keySetId);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
// The timestamp is at the start of rotation period 1000
long timestamp = rotationPeriodLength * 1000;
assertEquals(keySetId, transportKeyManager.addUnboundKeys(txn,
masterKey, timestamp, alice));
// The keys are unbound so no stream context should be returned
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId));
transportKeyManager.bindKeys(txn, contactId, keySetId);
// The keys are inactive so no stream context should be returned
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId));
transportKeyManager.activateKeys(txn, keySetId);
// The keys are active so a stream context should be returned
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
StreamContext ctx = transportKeyManager.getStreamContext(txn,
contactId);
assertNotNull(ctx);
assertEquals(contactId, ctx.getContactId());
assertEquals(transportId, ctx.getTransportId());
assertEquals(tagKey, ctx.getTagKey());
assertEquals(headerKey, ctx.getHeaderKey());
assertEquals(0L, ctx.getStreamNumber());
}
@Test
public void testRecognisingTagActivatesOutgoingKeys() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0, false);
Transaction txn = new Transaction(null, false);
// Keep a copy of the tags
List<byte[]> tags = new ArrayList<>();
expectAddUnboundKeysNoRotation(alice, transportKeys, txn);
context.checking(new Expectations() {{
// When the keys are bound, encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction(tags));
}
// Save the key binding
oneOf(db).bindTransportKeys(txn, contactId, transportId, keySetId);
// Encode a new tag after sliding the window
oneOf(transportCrypto).encodeTag(with(any(byte[].class)),
with(tagKey), with(PROTOCOL_VERSION),
with((long) REORDERING_WINDOW_SIZE));
will(new EncodeTagAction(tags));
// Save the reordering window (previous rotation period, base 1)
oneOf(db).setReorderingWindow(txn, keySetId, transportId, 999,
1, new byte[REORDERING_WINDOW_SIZE / 8]);
// Activate the keys
oneOf(db).setTransportKeysActive(txn, transportId, keySetId);
// Increment the stream counter
oneOf(db).incrementStreamCounter(txn, transportId, keySetId);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
// The timestamp is at the start of rotation period 1000
long timestamp = rotationPeriodLength * 1000;
assertEquals(keySetId, transportKeyManager.addUnboundKeys(txn,
masterKey, timestamp, alice));
transportKeyManager.bindKeys(txn, contactId, keySetId);
// The keys are inactive so no stream context should be returned
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId));
// Recognising an incoming tag should activate the outgoing keys
assertEquals(REORDERING_WINDOW_SIZE * 3, tags.size());
byte[] tag = tags.get(0);
StreamContext ctx = transportKeyManager.getStreamContext(txn, tag);
assertNotNull(ctx);
assertEquals(contactId, ctx.getContactId());
assertEquals(transportId, ctx.getTransportId());
assertEquals(tagKey, ctx.getTagKey());
assertEquals(headerKey, ctx.getHeaderKey());
assertEquals(0L, ctx.getStreamNumber());
// The keys are active so a stream context should be returned
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
ctx = transportKeyManager.getStreamContext(txn, contactId);
assertNotNull(ctx);
assertEquals(contactId, ctx.getContactId());
assertEquals(transportId, ctx.getTransportId());
assertEquals(tagKey, ctx.getTagKey());
assertEquals(headerKey, ctx.getHeaderKey());
assertEquals(0L, ctx.getStreamNumber());
}
@Test
public void testRemovingUnboundKeys() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0, false);
Transaction txn = new Transaction(null, false);
expectAddUnboundKeysNoRotation(alice, transportKeys, txn);
context.checking(new Expectations() {{
// Remove the unbound keys
oneOf(db).removeTransportKeys(txn, transportId, keySetId);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency);
// The timestamp is at the start of rotation period 1000
long timestamp = rotationPeriodLength * 1000;
assertEquals(keySetId, transportKeyManager.addUnboundKeys(txn,
masterKey, timestamp, alice));
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
transportKeyManager.removeKeys(txn, keySetId);
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
}
private void expectAddContactNoRotation(boolean alice,
TransportKeys transportKeys, Transaction txn) throws Exception {
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice, true);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000));
// Encode the tags (3 sets)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(3).of(transportCrypto).encodeTag(
with(any(byte[].class)), with(tagKey),
with(PROTOCOL_VERSION), with(i));
will(new EncodeTagAction());
}
// Rotate the transport keys (the keys are unaffected)
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(transportKeys));
// Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
will(returnValue(keySetId));
}});
}
private void expectAddUnboundKeysNoRotation(boolean alice,
TransportKeys transportKeys, Transaction txn) throws Exception {
context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice, false);
will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis();
will(returnValue(rotationPeriodLength * 1000));
// Rotate the transport keys (the keys are unaffected)
oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
will(returnValue(transportKeys));
// Save the unbound keys
oneOf(db).addTransportKeys(txn, null, transportKeys);
will(returnValue(keySetId));
}});
}
private TransportKeys createTransportKeys(long rotationPeriod,
long streamCounter) {
long streamCounter, boolean active) {
IncomingKeys inPrev = new IncomingKeys(tagKey, headerKey,
rotationPeriod - 1);
IncomingKeys inCurr = new IncomingKeys(tagKey, headerKey,
@@ -417,7 +609,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
IncomingKeys inNext = new IncomingKeys(tagKey, headerKey,
rotationPeriod + 1);
OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
rotationPeriod, streamCounter);
rotationPeriod, streamCounter, active);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
}

View File

@@ -352,6 +352,16 @@
/>
</activity>
<activity
android:name="org.briarproject.briar.android.test.TestDataActivity"
android:label="Create test data"
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.settings.SettingsActivity"
/>
</activity>
<activity
android:name="org.briarproject.briar.android.panic.PanicPreferencesActivity"
android:label="@string/panic_setting"
@@ -374,7 +384,12 @@
</activity>
<activity
android:name="org.briarproject.briar.android.panic.ExitActivity"
android:name="org.briarproject.briar.android.logout.ExitActivity"
android:theme="@android:style/Theme.NoDisplay">
</activity>
<activity
android:name=".android.logout.HideUiActivity"
android:theme="@android:style/Theme.NoDisplay">
</activity>

View File

@@ -62,6 +62,7 @@ import javax.inject.Inject;
import static android.app.Notification.DEFAULT_LIGHTS;
import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE;
import static android.app.Notification.VISIBILITY_SECRET;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
@@ -90,12 +91,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final int BLOG_POST_NOTIFICATION_ID = 6;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7;
// Channel IDs
private static final String CONTACT_CHANNEL_ID = "contacts";
private static final String GROUP_CHANNEL_ID = "groups";
private static final String FORUM_CHANNEL_ID = "forums";
private static final String BLOG_CHANNEL_ID = "blogs";
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
private static final Logger LOG =
@@ -174,6 +169,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
NotificationChannel nc =
new NotificationChannel(channelId, appContext.getString(name),
IMPORTANCE_DEFAULT);
nc.setLockscreenVisibility(VISIBILITY_SECRET);
nc.enableVibration(true);
nc.enableLights(true);
nc.setLightColor(
ContextCompat.getColor(appContext, R.color.briar_green_light));

View File

@@ -4,8 +4,11 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
@@ -17,19 +20,26 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logout.HideUiActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.ACTION_SHUTDOWN;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
@@ -61,6 +71,9 @@ public class BriarService extends Service {
private final AtomicBoolean created = new AtomicBoolean(false);
private final Binder binder = new BriarBinder();
@Nullable
private BroadcastReceiver receiver = null;
@Inject
protected DatabaseConfig databaseConfig;
// Fields that are accessed from background threads must be volatile
@@ -140,6 +153,19 @@ public class BriarService extends Service {
stopSelf();
}
}).start();
// Register for device shutdown broadcasts
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LOG.info("Device is shutting down");
shutdownFromBackground();
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_SHUTDOWN);
filter.addAction("android.intent.action.QUICKBOOT_POWEROFF");
filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF");
registerReceiver(receiver, filter);
}
private void showStartupFailureNotification(StartResult result) {
@@ -184,6 +210,7 @@ public class BriarService extends Service {
super.onDestroy();
LOG.info("Destroyed");
stopForeground(true);
if (receiver != null) unregisterReceiver(receiver);
// Stop the services in a background thread
new Thread(() -> {
if (started) lifecycleManager.stopServices();
@@ -194,7 +221,48 @@ public class BriarService extends Service {
public void onLowMemory() {
super.onLowMemory();
LOG.warning("Memory is low");
// FIXME: Work out what to do about it
shutdownFromBackground();
showLowMemoryShutdownNotification();
}
private void shutdownFromBackground() {
// Stop the service
stopSelf();
// Hide the UI
Intent i = new Intent(this, HideUiActivity.class);
i.addFlags(FLAG_ACTIVITY_NEW_TASK
| FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| FLAG_ACTIVITY_NO_ANIMATION
| FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
// Wait for shutdown to complete, then exit
new Thread(() -> {
try {
if (started) lifecycleManager.waitForShutdown();
} catch (InterruptedException e) {
LOG.info("Interrupted while waiting for shutdown");
}
LOG.info("Exiting");
System.exit(0);
}).start();
}
private void showLowMemoryShutdownNotification() {
androidExecutor.runOnUiThread(() -> {
NotificationCompat.Builder b = new NotificationCompat.Builder(
BriarService.this, FAILURE_CHANNEL_ID);
b.setSmallIcon(android.R.drawable.stat_notify_error);
b.setContentTitle(getText(
R.string.low_memory_shutdown_notification_title));
b.setContentText(getText(
R.string.low_memory_shutdown_notification_text));
Intent i = new Intent(this, SplashScreenActivity.class);
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
b.setAutoCancel(true);
Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(FAILURE_NOTIFICATION_ID, b.build());
});
}
/**

View File

@@ -70,6 +70,7 @@ import org.briarproject.briar.android.sharing.ShareForumFragment;
import org.briarproject.briar.android.sharing.ShareForumMessageFragment;
import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.test.TestDataActivity;
import dagger.Component;
@@ -147,6 +148,8 @@ public interface ActivityComponent {
void inject(SettingsActivity activity);
void inject(TestDataActivity activity);
void inject(ChangePasswordActivity activity);
void inject(IntroductionActivity activity);

View File

@@ -16,7 +16,7 @@ import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.login.PasswordActivity;
import org.briarproject.briar.android.panic.ExitActivity;
import org.briarproject.briar.android.logout.ExitActivity;
import java.util.logging.Logger;

View File

@@ -781,25 +781,18 @@ public class ConversationActivity extends BriarActivity
return;
}
PromptStateChangeListener listener = new PromptStateChangeListener() {
@Override
public void onPromptStateChanged(
MaterialTapTargetPrompt prompt, int state) {
if (state == STATE_DISMISSED ||
state == STATE_FINISHED) {
introductionOnboardingSeen();
}
}
};
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setPromptStateChangeListener(listener)
.show();
PromptStateChangeListener listener = (prompt, state) -> {
if (state == STATE_DISMISSED || state == STATE_FINISHED) {
introductionOnboardingSeen();
}
};
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setPromptStateChangeListener(listener)
.show();
});
}
@@ -865,10 +858,12 @@ introductionOnboardingSeen();
"Unknown Request Type");
}
loadMessages();
} catch (DbException | FormatException e) {
introductionResponseError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (ProtocolStateException e) {
// Action is no longer valid - reloading should solve the issue
if (LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
} catch (DbException e) {
// TODO show an error message
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
@@ -898,12 +893,9 @@ introductionOnboardingSeen();
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException, FormatException {
if (accept) {
introductionManager.acceptIntroduction(contactId, sessionId, time);
} else {
introductionManager.declineIntroduction(contactId, sessionId, time);
}
boolean accept, long time) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, time,
accept);
}
@DatabaseExecutor
@@ -921,18 +913,7 @@ introductionOnboardingSeen();
@DatabaseExecutor
private void respondToGroupRequest(SessionId id, boolean accept)
throws DbException {
try {
groupInvitationManager.respondToInvitation(contactId, id, accept);
} catch (ProtocolStateException e) {
// this action is no longer possible
}
}
private void introductionResponseError() {
runOnUiThreadUnlessDestroyed(() ->
Toast.makeText(ConversationActivity.this,
R.string.introduction_response_error,
Toast.LENGTH_SHORT).show());
groupInvitationManager.respondToInvitation(contactId, id, accept);
}
private ListenableFutureTask<String> getContactNameTask() {

View File

@@ -124,6 +124,11 @@ abstract class ConversationItem {
text = ctx.getString(
R.string.introduction_response_accepted_sent,
ir.getName());
if (ir.isSuccessPossible()) {
text += "\n\n" + ctx.getString(
R.string.introduction_response_accepted_sent_info,
ir.getName());
}
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,

View File

@@ -52,19 +52,15 @@ class ForumListAdapter
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
ui.avatar.setProblem(false);
ui.postCount.setText(ctx.getResources()
.getQuantityString(R.plurals.posts, postCount,
postCount));
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_secondary));
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
} else {
ui.avatar.setProblem(true);
ui.postCount.setText(ctx.getString(R.string.no_posts));
ui.postCount.setTextColor(
ContextCompat
.getColor(ctx, R.color.briar_text_tertiary));
ContextCompat.getColor(ctx, R.color.briar_text_tertiary));
}
// Date

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -11,7 +12,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -33,9 +33,10 @@ import im.delight.android.identicons.IdenticonDrawable;
import static android.app.Activity.RESULT_OK;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
public class IntroductionMessageFragment extends BaseFragment
implements TextInputListener {
@@ -125,14 +126,15 @@ public class IntroductionMessageFragment extends BaseFragment
new ContactId(contactId1));
Contact c2 = contactManager.getContact(
new ContactId(contactId2));
setUpViews(c1, c2);
boolean possible = introductionManager.canIntroduce(c1, c2);
setUpViews(c1, c2, possible);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
});
}
private void setUpViews(Contact c1, Contact c2) {
private void setUpViews(Contact c1, Contact c2, boolean possible) {
introductionActivity.runOnUiThreadUnlessDestroyed(() -> {
contact1 = c1;
contact2 = c2;
@@ -147,13 +149,22 @@ public class IntroductionMessageFragment extends BaseFragment
ui.contactName1.setText(c1.getAuthor().getName());
ui.contactName2.setText(c2.getAuthor().getName());
// set button action
ui.message.setListener(IntroductionMessageFragment.this);
// hide progress bar and show views
// hide progress bar
ui.progressBar.setVisibility(GONE);
ui.message.setSendButtonEnabled(true);
ui.message.showSoftKeyboard();
if (possible) {
// set button action
ui.message.setListener(IntroductionMessageFragment.this);
// show views
ui.notPossible.setVisibility(GONE);
ui.message.setVisibility(VISIBLE);
ui.message.setSendButtonEnabled(true);
ui.message.showSoftKeyboard();
} else {
ui.notPossible.setVisibility(VISIBLE);
ui.message.setVisibility(GONE);
}
});
}
@@ -175,7 +186,8 @@ public class IntroductionMessageFragment extends BaseFragment
ui.message.setSendButtonEnabled(false);
String msg = ui.message.getText().toString();
msg = StringUtils.truncateUtf8(msg, MAX_INTRODUCTION_MESSAGE_LENGTH);
if (msg.equals("")) msg = null;
else msg = StringUtils.truncateUtf8(msg, MAX_REQUEST_MESSAGE_LENGTH);
makeIntroduction(contact1, contact2, msg);
// don't wait for the introduction to be made before finishing activity
@@ -184,13 +196,14 @@ public class IntroductionMessageFragment extends BaseFragment
introductionActivity.supportFinishAfterTransition();
}
private void makeIntroduction(Contact c1, Contact c2, String msg) {
private void makeIntroduction(Contact c1, Contact c2,
@Nullable String msg) {
introductionActivity.runOnDbThread(() -> {
// actually make the introduction
try {
long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(c1, c2, msg, timestamp);
} catch (DbException | FormatException e) {
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
introductionError();
}
@@ -208,6 +221,7 @@ public class IntroductionMessageFragment extends BaseFragment
private final ProgressBar progressBar;
private final CircleImageView avatar1, avatar2;
private final TextView contactName1, contactName2;
private final TextView notPossible;
private final TextInputView message;
private ViewHolder(View v) {
@@ -216,6 +230,7 @@ public class IntroductionMessageFragment extends BaseFragment
avatar2 = v.findViewById(R.id.avatarContact2);
contactName1 = v.findViewById(R.id.nameContact1);
contactName2 = v.findViewById(R.id.nameContact2);
notPossible = v.findViewById(R.id.introductionNotPossibleView);
message = v.findViewById(R.id.introductionMessageView);
}
}

View File

@@ -14,6 +14,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;
@@ -52,6 +54,8 @@ import javax.inject.Provider;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -84,6 +88,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
private ImageView qrCode;
private TextView mainProgressTitle;
private ViewGroup mainProgressContainer;
private boolean fullscreen = false;
private boolean gotRemotePayload;
private volatile boolean gotLocalPayload;
@@ -128,6 +133,34 @@ public class ShowQrCodeFragment extends BaseEventFragment
qrCode = view.findViewById(R.id.qr_code);
mainProgressTitle = view.findViewById(R.id.title_progress_bar);
mainProgressContainer = view.findViewById(R.id.container_progress);
ImageView fullscreenButton = view.findViewById(R.id.fullscreen_button);
fullscreenButton.setOnClickListener(v -> {
View qrCodeContainer = view.findViewById(R.id.qr_code_container);
LinearLayout cameraOverlay = view.findViewById(R.id.camera_overlay);
LayoutParams statusParams, qrCodeParams;
if (fullscreen) {
// Shrink the QR code container to fill half its parent
if (cameraOverlay.getOrientation() == HORIZONTAL) {
statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
} else {
statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
}
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_black_48dp);
} else {
// Grow the QR code container to fill its parent
statusParams = new LayoutParams(0, 0, 0f);
qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_exit_black_48dp);
}
statusView.setLayoutParams(statusParams);
qrCodeContainer.setLayoutParams(qrCodeParams);
cameraOverlay.invalidate();
fullscreen = !fullscreen;
});
}
@Override
@@ -284,6 +317,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
new AsyncTask<Void, Void, Bitmap>() {
@Override
@Nullable
protected Bitmap doInBackground(Void... params) {
byte[] payloadBytes = payloadEncoder.encode(payload);
if (LOG.isLoggable(INFO)) {

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.panic;
package org.briarproject.briar.android.logout;
import android.os.Build;
import android.os.Bundle;

View File

@@ -0,0 +1,20 @@
package org.briarproject.briar.android.logout;
import android.os.Bundle;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
public class HideUiActivity extends BaseActivity {
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
finish();
}
@Override
public void injectActivity(ActivityComponent component) {
}
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.fragment;
package org.briarproject.briar.android.logout;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -7,15 +7,17 @@ import android.view.ViewGroup;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class SignOutFragment extends BaseFragment {
private static final String TAG = SignOutFragment.class.getName();
public static final String TAG = SignOutFragment.class.getName();
@Override
public View onCreateView(LayoutInflater inflater,
public View onCreateView(@Nonnull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_sign_out, container, false);
@@ -30,5 +32,4 @@ public class SignOutFragment extends BaseFragment {
public void injectFragment(ActivityComponent component) {
// no need to inject
}
}

View File

@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.DrawerLayout;
@@ -22,6 +23,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
@@ -35,7 +37,7 @@ import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.forum.ForumListFragment;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.fragment.SignOutFragment;
import org.briarproject.briar.android.logout.SignOutFragment;
import org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning;
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
import org.briarproject.briar.android.settings.SettingsActivity;
@@ -51,6 +53,7 @@ import static android.support.v4.view.GravityCompat.START;
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
@@ -73,6 +76,8 @@ public class NavDrawerActivity extends BriarActivity implements
@Inject
NavDrawerController controller;
@Inject
LifecycleManager lifecycleManager;
private DrawerLayout drawerLayout;
private NavigationView navigation;
@@ -128,7 +133,9 @@ public class NavDrawerActivity extends BriarActivity implements
initializeTransports(getLayoutInflater());
transportsView.setAdapter(transportsAdapter);
if (state == null) {
if (lifecycleManager.getLifecycleState().isAfter(RUNNING)) {
showSignOutFragment();
} else if (state == null) {
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
}
@@ -212,19 +219,23 @@ public class NavDrawerActivity extends BriarActivity implements
public void onBackPressed() {
if (drawerLayout.isDrawerOpen(START)) {
drawerLayout.closeDrawer(START);
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
getSupportFragmentManager()
.findFragmentByTag(ContactListFragment.TAG) == null) {
} else {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(SignOutFragment.TAG) != null) {
finish();
} else if (fm.getBackStackEntryCount() == 0
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
/*
* This makes sure that the first fragment (ContactListFragment) the
* user sees is the same as the last fragment the user sees before
* exiting. This models the typical Google navigation behaviour such
* as in Gmail/Inbox.
*/
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
} else {
super.onBackPressed();
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
} else {
super.onBackPressed();
}
}
}
@@ -240,10 +251,15 @@ public class NavDrawerActivity extends BriarActivity implements
drawerToggle.onConfigurationChanged(newConfig);
}
private void signOut() {
private void showSignOutFragment() {
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
startFragment(new SignOutFragment());
}
private void signOut() {
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
signOut(false);
finish();
}
private void startFragment(BaseFragment fragment, int itemId) {

View File

@@ -74,9 +74,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
if (group.isEmpty()) {
postCount.setVisibility(GONE);
date.setVisibility(GONE);
avatar.setProblem(true);
status
.setText(ctx.getString(R.string.groups_group_is_empty));
status.setText(ctx.getString(R.string.groups_group_is_empty));
status.setVisibility(VISIBLE);
} else {
// Message Count
@@ -91,7 +89,6 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
long lastUpdate = group.getTimestamp();
date.setText(UiUtils.formatDate(ctx, lastUpdate));
date.setVisibility(VISIBLE);
avatar.setProblem(false);
status.setVisibility(GONE);
}
remove.setVisibility(GONE);

View File

@@ -173,7 +173,12 @@ public class BriarReportPrimer implements ReportPrimer {
WifiInfo wifiInfo = wm.getConnectionInfo();
if (wifiInfo != null) {
int ip = wifiInfo.getIpAddress(); // Nice API, Google
customData.put("Wi-Fi address", StringUtils.ipToString(ip));
int ip1 = ip & 0xFF;
int ip2 = (ip >> 8) & 0xFF;
int ip3 = (ip >> 16) & 0xFF;
int ip4 = (ip >> 24) & 0xFF;
String address = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
customData.put("Wi-Fi address", address);
}
}

View File

@@ -1,12 +1,13 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
@@ -29,7 +30,6 @@ import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.UserFeedback;
import org.briarproject.briar.api.test.TestDataCreator;
import java.util.logging.Logger;
@@ -44,6 +44,10 @@ import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
@@ -53,6 +57,10 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
@@ -96,8 +104,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
@Inject
AndroidExecutor androidExecutor;
@Inject
TestDataCreator testDataCreator;
@Override
public void onAttach(Context context) {
@@ -132,33 +138,10 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
if (Build.VERSION.SDK_INT >= 21) {
if (SDK_INT >= 21) {
notifyLockscreen.setVisible(true);
notifyLockscreen.setOnPreferenceChangeListener(this);
}
notifySound.setOnPreferenceClickListener(preference -> {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
startActivityForResult(i, REQUEST_RINGTONE);
return true;
});
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
preference -> {
@@ -167,14 +150,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
});
Preference testData = findPreference("pref_key_test_data");
if (IS_DEBUG_BUILD) {
testData.setOnPreferenceClickListener(preference -> {
LOG.info("Creating test data");
testDataCreator.createTestData();
getActivity().finish();
return true;
});
} else {
if (!IS_DEBUG_BUILD) {
testData.setVisible(false);
}
@@ -220,36 +196,54 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setValue(Boolean.toString(btSetting));
torNetwork.setValue(Integer.toString(torSetting));
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyLockscreen.setChecked(settings.getBoolean(
PREF_NOTIFY_LOCK_SCREEN, false));
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
if (SDK_INT < 26) {
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
notifyLockscreen.setChecked(settings.getBoolean(
PREF_NOTIFY_LOCK_SCREEN, false));
notifySound.setOnPreferenceClickListener(
pref -> onNotificationSoundClicked());
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName =
settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = ringtoneName;
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
} else {
text = getString(R.string.notify_sound_setting_disabled);
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifyLockscreen.setVisible(false);
notifySound.setVisible(false);
}
notifySound.setSummary(text);
setSettingsEnabled(true);
});
}
@@ -266,6 +260,41 @@ public class SettingsFragment extends PreferenceFragmentCompat
notifySound.setEnabled(enabled);
}
@TargetApi(26)
private void setupNotificationPreference(CheckBoxPreference pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setOnPreferenceClickListener(clickedPref -> {
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, getContext().getPackageName())
.putExtra(EXTRA_CHANNEL_ID, channelId);
startActivity(intent);
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri =
settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
startActivityForResult(i, REQUEST_RINGTONE);
return true;
}
private void triggerFeedback() {
androidExecutor.runOnBackgroundThread(() -> ACRA.getErrorReporter()
.handleException(new UserFeedback(), false));

View File

@@ -1,14 +1,20 @@
package org.briarproject.briar.android.splash;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import org.briarproject.briar.R;
import static android.content.Intent.ACTION_VIEW;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
public class ExpiredActivity extends Activity {
public class ExpiredActivity extends AppCompatActivity
implements OnClickListener {
@Override
public void onCreate(Bundle state) {
@@ -17,5 +23,13 @@ public class ExpiredActivity extends Activity {
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
setContentView(R.layout.activity_expired);
findViewById(R.id.download_briar_button).setOnClickListener(this);
}
@Override
public void onClick(View v) {
Uri uri = Uri.parse("https://briarproject.org/download.html");
startActivity(new Intent(ACTION_VIEW, uri));
finish();
}
}

View File

@@ -0,0 +1,98 @@
package org.briarproject.briar.android.test;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.MenuItem;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.api.test.TestDataCreator;
import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
public class TestDataActivity extends BriarActivity {
@Inject
TestDataCreator testDataCreator;
private TextView[] textViews = new TextView[5];
private SeekBar[] seekBars = new SeekBar[5];
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}
setContentView(R.layout.activity_test_data);
textViews[0] = findViewById(R.id.textViewContactsSb);
textViews[1] = findViewById(R.id.textViewMessagesSb);
textViews[2] = findViewById(R.id.TextViewBlogPostsSb);
textViews[3] = findViewById(R.id.TextViewForumsSb);
textViews[4] = findViewById(R.id.TextViewForumMessagesSb);
seekBars[0] = findViewById(R.id.seekBarContacts);
seekBars[1] = findViewById(R.id.seekBarMessages);
seekBars[2] = findViewById(R.id.seekBarBlogPosts);
seekBars[3] = findViewById(R.id.seekBarForums);
seekBars[4] = findViewById(R.id.seekBarForumMessages);
for (int i = 0; i < 5; i++) {
final TextView textView = textViews[i];
seekBars[i].setOnSeekBarChangeListener(
new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar,
int progress, boolean fromUser) {
textView.setText(String.valueOf(progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
findViewById(R.id.buttonCreateTestData).setOnClickListener(
v -> createTestData());
}
private void createTestData() {
testDataCreator.createTestData(seekBars[0].getProgress(),
seekBars[1].getProgress(), seekBars[2].getProgress(),
seekBars[3].getProgress(), seekBars[4].getProgress());
Intent intent = new Intent(this, NavDrawerActivity.class);
intent.addFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return false;
}
}

View File

@@ -5,7 +5,6 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -26,7 +25,6 @@ public class TextAvatarView extends FrameLayout {
private final AppCompatTextView character;
private final CircleImageView background;
private final TextView badge;
private int unreadCount;
public TextAvatarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -49,30 +47,14 @@ public class TextAvatarView extends FrameLayout {
}
public void setUnreadCount(int count) {
unreadCount = count;
if (count > 0) {
badge.setBackgroundResource(R.drawable.bubble);
badge.setText(String.valueOf(count));
badge.setTextColor(ContextCompat.getColor(getContext(),
R.color.briar_text_primary_inverse));
badge.setVisibility(VISIBLE);
} else {
badge.setVisibility(INVISIBLE);
}
}
public void setProblem(boolean problem) {
if (problem) {
badge.setBackgroundResource(R.drawable.bubble_problem);
badge.setText("!");
badge.setTextColor(ContextCompat
.getColor(getContext(), R.color.briar_primary));
badge.setVisibility(VISIBLE);
} else {
setUnreadCount(unreadCount);
}
}
public void setBackgroundBytes(byte[] bytes) {
int r = getByte(bytes, 0) * 3 / 4 + 96;
int g = getByte(bytes, 1) * 3 / 4 + 96;

View File

@@ -21,6 +21,12 @@ public interface AndroidNotificationManager {
String PREF_NOTIFY_VIBRATION = "notifyVibration";
String PREF_NOTIFY_LOCK_SCREEN = "notifyLockScreen";
// Channel IDs
String CONTACT_CHANNEL_ID = "contacts";
String GROUP_CHANNEL_ID = "groups";
String FORUM_CHANNEL_ID = "forums";
String BLOG_CHANNEL_ID = "blogs";
// Content URIs for pending intents
String CONTACT_URI = "content://org.briarproject.briar/contact";
String GROUP_URI = "content://org.briarproject.briar/group";

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="@dimen/unread_bubble_size"/>
<padding
android:left="@dimen/unread_bubble_padding_horizontal"
android:right="@dimen/unread_bubble_padding_horizontal"/>
<solid
android:color="@color/briar_gold"/>
<stroke
android:color="@color/briar_primary"
android:width="@dimen/avatar_border_width"/>
</shape>

View File

@@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<vector android:height="48dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
</vector>

View File

@@ -15,39 +15,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
android:baselineAligned="false">
<FrameLayout
<LinearLayout
android:id="@+id/status_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_weight="1"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<LinearLayout
android:id="@+id/status_container"
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</FrameLayout>
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
<FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/qr_code"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:layout_gravity="center"/>
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/fullscreen_button"
android:background="?selectableItemBackground"
android:src="@drawable/ic_fullscreen_black_48dp"
android:alpha="0.54"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/show_qr_code_fullscreen"/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -1,9 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/expiry_date_reached"
android:textSize="@dimen/text_size_large"/>
android:orientation="vertical">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_large"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"
android:gravity="center"
android:text="@string/expiry_date_reached"
android:textSize="@dimen/text_size_large"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"
android:gravity="center"
android:text="@string/download_briar"
android:textSize="@dimen/text_size_large"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"
android:gravity="center"
android:text="@string/create_new_account"
android:textSize="@dimen/text_size_large"/>
<Button
android:id="@+id/download_briar_button"
style="@style/BriarButton.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"
android:text="@string/download_briar_button"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp">
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textViewContacts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number of contacts"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<SeekBar
android:id="@+id/seekBarContacts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="50"
android:progress="20"
app:layout_constraintEnd_toStartOf="@+id/textViewContactsSb"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewContacts"/>
<TextView
android:id="@+id/textViewContactsSb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:text="20"
app:layout_constraintBottom_toBottomOf="@+id/seekBarContacts"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textViewMessages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number of messages per contact"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBarContacts"/>
<SeekBar
android:id="@+id/seekBarMessages"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="50"
android:paddingTop="5dp"
android:progress="15"
app:layout_constraintEnd_toStartOf="@+id/textViewMessagesSb"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewMessages"/>
<TextView
android:id="@+id/textViewMessagesSb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:text="20"
app:layout_constraintBottom_toBottomOf="@+id/seekBarMessages"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textViewBlogPosts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number of blog posts"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBarMessages"/>
<SeekBar
android:id="@+id/seekBarBlogPosts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="50"
android:paddingTop="5dp"
android:progress="30"
app:layout_constraintEnd_toStartOf="@+id/TextViewBlogPostsSb"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewBlogPosts"/>
<TextView
android:id="@+id/TextViewBlogPostsSb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:text="20"
app:layout_constraintBottom_toBottomOf="@+id/seekBarBlogPosts"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textViewForums"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number of forums"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBarBlogPosts"/>
<SeekBar
android:id="@+id/seekBarForums"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="10"
android:paddingTop="5dp"
android:progress="3"
app:layout_constraintEnd_toStartOf="@+id/TextViewForumsSb"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewForums"/>
<TextView
android:id="@+id/TextViewForumsSb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:text="20"
app:layout_constraintBottom_toBottomOf="@+id/seekBarForums"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textViewForumMessages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number of forum messages"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBarForums"/>
<SeekBar
android:id="@+id/seekBarForumMessages"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="50"
android:paddingTop="5dp"
android:progress="30"
app:layout_constraintEnd_toStartOf="@+id/TextViewForumMessagesSb"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewForumMessages"/>
<TextView
android:id="@+id/TextViewForumMessagesSb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:text="20"
app:layout_constraintBottom_toBottomOf="@+id/seekBarForumMessages"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/buttonCreateTestData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Create test data"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBarForumMessages"/>
</android.support.constraint.ConstraintLayout>
</ScrollView>
</android.support.constraint.ConstraintLayout>

View File

@@ -15,39 +15,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
android:baselineAligned="false">
<FrameLayout
<LinearLayout
android:id="@+id/status_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<LinearLayout
android:id="@+id/status_container"
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</FrameLayout>
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
<FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/qr_code"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:layout_gravity="center"/>
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/fullscreen_button"
android:background="?selectableItemBackground"
android:src="@drawable/ic_fullscreen_black_48dp"
android:alpha="0.54"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/show_qr_code_fullscreen"/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -1,23 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="128dp"
android:layout_height="128dp"
android:scaleType="center"
android:src="@drawable/startup_lock"
android:tint="@color/briar_primary"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintEnd_toEndOf="@+id/imageView"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView"/>
<TextView
android:id="@+id/title_progress_bar"
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progressBar"
android:layout_centerHorizontal="true"
android:paddingTop="@dimen/margin_large"
android:text="@string/progress_title_logout"/>
android:layout_margin="8dp"
android:text="@string/progress_title_logout"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView"/>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>

View File

@@ -94,13 +94,25 @@
android:layout_gravity="center"
tools:visibility="gone"/>
<TextView
android:id="@+id/introductionNotPossibleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_activity_horizontal"
android:text="@string/introduction_not_possible"
android:textSize="@dimen/text_size_large"
android:visibility="gone"
tools:visibility="visible"/>
<org.briarproject.briar.android.view.LargeTextInputView
android:id="@+id/introductionMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:buttonText="@string/introduction_button"
app:hint="@string/introduction_message_hint"
app:maxLines="5"/>
app:maxLines="5"
tools:visibility="visible"/>
</LinearLayout>
</ScrollView>

View File

@@ -75,9 +75,7 @@
<string name="show_onboarding">Показване на помощен диалог</string>
<string name="help">Помощ</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Няма добавени контакти.\n\nНатиснете плюса и следвайте инструкциите, за да добавите приятели.\n\nВнимание: Трябва да се срещнете лично с човека, когото искате да добавите в Контакти. По този начин никой не може да се представи за вас или да чете съобщенията ви в бъдеще.</string>
<string name="date_no_private_messages">Няма съобщения.</string>
<string name="no_private_messages">Тук се показват съобщенията ви.\n\nНяма съобщения.</string>
<string name="message_hint">Напиши съобщение</string>
<string name="delete_contact">Изтрий контакт</string>
<string name="dialog_title_delete_contact">Потвърди изтриването на контакт</string>
@@ -122,7 +120,6 @@
<item quantity="other">%d добавени нови контакти.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Не сте включен в група.\n\nНатиснете плюса, за да създадете своя група, или помолете контактите си да ви добавят в техните групи.</string>
<string name="groups_created_by">Създаден от %s</string>
<plurals name="messages">
<item quantity="one">%d съобщение</item>
@@ -175,12 +172,10 @@
<string name="groups_reveal_visible_revealed_by_contact">Връзка с контакта се вижда от групата (разкрита от %s)</string>
<string name="groups_reveal_invisible">Връзка с контакта не се вижда от групата</string>
<!--Forums-->
<string name="no_forums">Няма добавени форуми.\n\nНатиснете плюса, за да създадете нов форум.\n\nМожете също да помолите контактите си да споделят форуми с вас.</string>
<string name="create_forum_title">Създаване на форум</string>
<string name="choose_forum_hint">Изберете име за форума</string>
<string name="create_forum_button">Създай форум</string>
<string name="forum_created_toast">Форумът е създаден</string>
<string name="no_forum_posts">Форумът е празен.\n\nВъведете текст, за да съставите първата публикация.\n\nСподелете този форум с други ваши контакти!</string>
<string name="no_posts">Няма публикации</string>
<plurals name="posts">
<item quantity="one">%d публикация</item>
@@ -192,23 +187,17 @@
<string name="btn_reply">Отговори</string>
<string name="forum_leave">Напусни форума</string>
<string name="dialog_title_leave_forum">Потвърдете напускането на форума</string>
<string name="dialog_message_leave_forum">Сигурни ли сте, че искате да напуснете този форум? Контактите, с които сте споделили този форум, ще престанат да получават ъпдейти от него.</string>
<string name="dialog_button_leave">Напусни</string>
<string name="forum_left_toast">Напуснахте форума</string>
<!--Forum Sharing-->
<string name="forum_share_button">Сподели форум</string>
<string name="contacts_selected">Избрани контакти</string>
<string name="activity_share_toolbar_header">Избиране на контакти</string>
<string name="no_contacts_selector">Няма добавени контакти.\n\nМоля, върнете се тук, когато добавите първия си контакт.</string>
<string name="forum_shared_snackbar">Форумът е споделен с избраните контакти</string>
<string name="forum_share_message">Добавете съобщение (незадължително)</string>
<string name="forum_share_error">Възникна грешка при споделянето на този форум.</string>
<string name="forum_invitation_received">%1$s сподели форума \"%2$s\" с вас.</string>
<string name="forum_invitation_sent">Споделихте форума \"%1$s\" с %2$s.</string>
<string name="forum_invitations_title">Покани във форум</string>
<string name="forum_invitation_exists">Вече приехте поканата за този форум. Приемането на още покани ще подпомогне развитието на форума. </string>
<string name="forum_joined_toast">Включихте се във форума</string>
<string name="forum_declined_toast">Поканата във форум е отказана</string>
<string name="shared_by_format">Споделен от %s</string>
<string name="forum_invitation_already_sharing">Вече е споделен</string>
<string name="forum_invitation_response_accepted_sent">Приехте поканата за форум на %s.</string>
@@ -224,19 +213,14 @@
</plurals>
<string name="nobody">Никого</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Блогът е празен.\n\nИли авторът още не е публикувал нищо, или е нужно да е онлайн човекът, който е споделил този блог с вас, за да се синхронизират публикациите.</string>
<string name="read_more">прочети още</string>
<string name="blogs_write_blog_post">Нова блог публикация</string>
<string name="blogs_write_blog_post_body_hint">Въведете новата публикация тук</string>
<string name="blogs_publish_blog_post">Публикуване</string>
<string name="blogs_blog_post_created">Блог публикацията е създадена</string>
<string name="blogs_blog_post_received">Нова блог публикация</string>
<string name="blogs_blog_post_scroll_to">Отвори</string>
<string name="blogs_feed_empty_state">Това е глобалната блог емисия.\n\nНикой още не е публикувал нищо.\n\nБъдете първия и натиснете писалката, за да напишете първата блог публикация.</string>
<string name="blogs_remove_blog">Премахване на блог</string>
<string name="blogs_remove_blog_dialog_message">Сигурни ли сте, че искате да премахнете този блог и всички публикации?\nБлогът няма да бъдат премахнат от устройствата на други хора.</string>
<string name="blogs_remove_blog_ok">Премахни блог</string>
<string name="blogs_blog_removed">Блогът е премахнат</string>
<string name="blogs_remove_blog_ok">Премахване</string>
<string name="blogs_reblog_comment_hint">Добавете съобщение (незадължително)</string>
<string name="blogs_reblog_button">Реблог</string>
<!--Blog Sharing-->
@@ -251,8 +235,6 @@
<string name="blogs_sharing_invitation_received">%1$s сподели блога \"%2$s\" с вас.</string>
<string name="blogs_sharing_invitation_sent">Споделихте блога \"%1$s\" с %2$s.</string>
<string name="blogs_sharing_invitations_title">Блог покани</string>
<string name="blogs_sharing_joined_toast">Абонирахте се за блога</string>
<string name="blogs_sharing_declined_toast">Поканата в блог е отказана</string>
<string name="sharing_status_blog">Всеки абонат на блога може да го сподели с контактите си. Споделяте този блог със следните контакти. Възможно е да има и други, които не можете да видите.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Внасяне на RSS емисия</string>
@@ -264,10 +246,8 @@
<string name="blogs_rss_feeds_manage_author">Автор:</string>
<string name="blogs_rss_feeds_manage_updated">Последно актуализиране:</string>
<string name="blogs_rss_remove_feed">Премахване на емисия</string>
<string name="blogs_rss_remove_feed_dialog_message">Сигурни ли сте, че искате да премахнете тази емисия и всички нейни публикации?\nВсички споделени от вас публикации няма да бъдат премахнати от устройствата на други хора.</string>
<string name="blogs_rss_remove_feed_ok">Емисията е премахната</string>
<string name="blogs_rss_remove_feed_ok">Премахване</string>
<string name="blogs_rss_feeds_manage_delete_error">Емисията не можа да бъде изтрита!</string>
<string name="blogs_rss_feeds_manage_empty_state">Нямате добавени RSS емисии.\n\nНатиснете плюса в горния десен ъгъл на екрана, за да добавите нова емисия.</string>
<string name="blogs_rss_feeds_manage_error">Възникна проблем при зареждането на емисиите ви. Моля, опитайте пак по-късно.</string>
<!--Settings Network-->
<string name="network_settings_title">Мрежа</string>

View File

@@ -134,8 +134,10 @@
<!--Forum Sharing-->
<string name="forum_share_message">Ouzhpennañ ur gemennadenn (diret)</string>
<!--Blogs-->
<string name="blogs_remove_blog_ok">Dilemel</string>
<!--Blog Sharing-->
<!--RSS Feeds-->
<string name="blogs_rss_remove_feed_ok">Dilemel</string>
<!--Settings Network-->
<!--Settings Security and Panic-->
<string name="lock_setting_title">Digevreañ</string>

View File

@@ -33,8 +33,6 @@
<string name="startup_failed_notification_title">Briar no s\'ha pogut iniciar</string>
<string name="startup_failed_notification_text">Toqueu per obtenir més informació.</string>
<string name="startup_failed_activity_title">Error iniciant Briar</string>
<string name="startup_failed_db_error">Per alguna raó, la vostra base de dades de Briar està malmesa més enllà de la reparació. El vostre compte, les vostres dades i tots els vostres contactes es perdran. Malauradament, necessiteu tornar a instal·lar Briar i configurar un nou compte escollint «He oblidat la meva contrasenya» quan se us demani la contrasenya.</string>
<string name="startup_failed_data_too_old_error">El vostre compte s\'ha creat amb una versió anterior d\'aquesta aplicació i no es pot obrir amb aquesta versió. Heu de tornar a instal·lar la versió antiga o eliminar el vostre antic compte escollint «He oblidat la meva contrasenya» quan se us demani la contrasenya.</string>
<string name="startup_failed_data_too_new_error">Aquesta versió de l\'aplicació és massa antiga. Actualitzeu a la versió més recent i torneu-ho a provar.</string>
<string name="startup_failed_service_error">Briar no ha pogut engegar un connector necessari. La solució sol ser reinstal·lar Briar. De tota manera, tingueu en compte que si ho feu perdreu el vostre compte i totes les dades associades, doncs Briar no usa servidors centrals per desar les vostres dades.</string>
<plurals name="expiry_warning">
@@ -43,6 +41,8 @@
</plurals>
<string name="expiry_update">S\'ha ampliat la data de venciment de la prova. El vostre compte caducarà ara en %d dies.</string>
<string name="expiry_date_reached">Aquest programa ha caducat.\nGràcies per haver-lo provat!</string>
<string name="startup_open_database">S\'està desxifrant la base de dades...</string>
<string name="startup_migrate_database">S\'està actualitzant la base de dades...</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Obre el calaix de navegació</string>
<string name="nav_drawer_close_description">Tanca el calaix de navegació</string>
@@ -99,9 +99,7 @@
<string name="help">Ajuda</string>
<string name="sorry">Ens sap greu</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Sembla que sou nouvingut i no teniu contactes encara.\n\nToqueu la icona + en la part superior i seguiu les instruccions per a afegir amics a la llista.\n\nRecordeu: afegiu només contactes cara a cara per a evitar que algú es faci passar per vós o pugui llegir els vostres missatges en el futur. </string>
<string name="date_no_private_messages">Sense missatges.</string>
<string name="no_private_messages">Aquesta és la vista de conversa.\n\nSembla que hi manca una part de la conversa.\n\nNomés cal que toqueu el camp d\'entrada a la part inferior per iniciar una conversa.</string>
<string name="message_hint">Escriu el missatge.</string>
<string name="delete_contact">Suprimeix contacte</string>
<string name="dialog_title_delete_contact">Confirma la supressió del contacte</string>
@@ -149,7 +147,6 @@
<item quantity="other">S\'ha afegit %d nous contactes.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">No pertanyeu a cap grup.\n\nToqueu la icona + en la part superior per a crear un grup o demaneu als vostres contactes que us conviden a un grup.</string>
<string name="groups_created_by">Creat per 1%s</string>
<plurals name="messages">
<item quantity="one">1%d missatge</item>
@@ -202,12 +199,11 @@
<string name="groups_reveal_visible_revealed_by_contact">La relació del contacte és visible per al grup (revelat per %s)</string>
<string name="groups_reveal_invisible">La relació del contacte no és visible per al grup</string>
<!--Forums-->
<string name="no_forums">No teniu cap fòrum encara.\n\nPer què no en creeu un de nou tocant el + que hi ha a la part superior dreta?\n\nTambé podeu demanar els vostres contactes que us conviden a fòrums.</string>
<string name="create_forum_title">Crea un fòrum</string>
<string name="choose_forum_hint">Trieu un nom per al fòrum</string>
<string name="create_forum_button">Crea el fòrum</string>
<string name="forum_created_toast">S\'ha creat el fòrum</string>
<string name="no_forum_posts">Aquest fòrum està buit.\n\nUtilitzeu la icona de la ploma a la part superior per redactar la primera publicació.\n\nEsteu aquí sol? Compartiu aquest fòrum amb els vostres contactes!</string>
<string name="no_forum_posts">No hi ha publicacions per mostrar</string>
<string name="no_posts">No hi ha publicacions</string>
<plurals name="posts">
<item quantity="one">%d publicacio</item>
@@ -219,23 +215,17 @@
<string name="btn_reply">Respon</string>
<string name="forum_leave">Abandona el fòrum</string>
<string name="dialog_title_leave_forum">Confirmeu la sortida del Forum</string>
<string name="dialog_message_leave_forum">Esteu segur que voleu sortir del fòrum? Els contactes amb qui heu compartit aquest fòrum podrien deixar de rebre\'n actualitzacions.</string>
<string name="dialog_button_leave">Abandona</string>
<string name="forum_left_toast">Abandona el fòrum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Comparteix el fòrum</string>
<string name="contacts_selected">Contactes seleccionats</string>
<string name="activity_share_toolbar_header">Tria contactes</string>
<string name="no_contacts_selector">Sembla que sou nou aquí i no teniu cap contacte.\n\nTorneu aquí després d\'afegir el primer contacte.</string>
<string name="forum_shared_snackbar">Fòrum compartit amb els contactes seleccionats</string>
<string name="forum_share_message">Afegiu un missatge (opcional)</string>
<string name="forum_share_error">S\'ha produït un error en compartir aquest fòrum.</string>
<string name="forum_invitation_received">%1$s ha compartit el fòrum «%2$s» amb vós.</string>
<string name="forum_invitation_sent">Heu compartit el fòrum «%1$s» amb %2$s.</string>
<string name="forum_invitations_title">Invitacions al fòrum</string>
<string name="forum_invitation_exists">Ja heu acceptat una invitació a aquest fòrum. L\'acceptació de més invitacions augmentarà i enfortirà la comunicació al fòrum.</string>
<string name="forum_joined_toast">Us hi heu unit al fòrum</string>
<string name="forum_declined_toast">S\'ha rebutjat la invitació al fòrum</string>
<string name="shared_by_format">Compartit per 1%s</string>
<string name="forum_invitation_already_sharing">Ja esteu compartint</string>
<string name="forum_invitation_response_accepted_sent">Heu acceptat la invitació del fòrum de %s.</string>
@@ -251,19 +241,15 @@
</plurals>
<string name="nobody">Ningú</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Aquest blog està buit.\n\nPotser l\'autor encara no hi ha escrit res o que la persona que us ha compartit aquest blog hagi de connectar-se, llavors es podran sincronitzar les publicacions.</string>
<string name="blogs_other_blog_empty_state">No hi ha publicacions per mostrar</string>
<string name="read_more">llegir més</string>
<string name="blogs_write_blog_post">Escriviu una publicació de blog</string>
<string name="blogs_write_blog_post_body_hint">Escriviu la vostra publicació al blog aquí</string>
<string name="blogs_publish_blog_post">Publica</string>
<string name="blogs_blog_post_created">S\'ha creat la publicació al blog</string>
<string name="blogs_blog_post_received">S\'ha rebut una nova publicació al blog</string>
<string name="blogs_blog_post_scroll_to">Desplaça</string>
<string name="blogs_feed_empty_state">Aquestes són totes les entrades del blog.\n\nSembla que ningú ha publicat res encara.\n\nSigueu el primer i premeu la icona del llapis per escriure una publicació nova al blog.</string>
<string name="blogs_remove_blog">Elimina el blog</string>
<string name="blogs_remove_blog_dialog_message">Esteu segur que voleu esborrar aquest blog i totes les seves publicacions?\nTingueu present que això no esborrarà el blog dels dispositius d\'altres persones.</string>
<string name="blogs_remove_blog_ok">Elimina el blog</string>
<string name="blogs_blog_removed">S\'ha eliminat el blog</string>
<string name="blogs_remove_blog_ok">Eliminar</string>
<string name="blogs_reblog_comment_hint">Afegiu un comentari (opcional)</string>
<string name="blogs_reblog_button">Rebloga</string>
<!--Blog Sharing-->
@@ -278,8 +264,6 @@
<string name="blogs_sharing_invitation_received">%1$s us ha compartit el blog \"%2$s\".</string>
<string name="blogs_sharing_invitation_sent">Heu compartit el blog \"%1$s\" amb %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitacions al blog</string>
<string name="blogs_sharing_joined_toast">Subscrit al blog</string>
<string name="blogs_sharing_declined_toast">Invitació al blog refusada</string>
<string name="sharing_status_blog">Qualsevol subscrit a un blog el pot compartir amb els seus contactes. Esteu compartint aquest blog amb els següents contactes. També hi pot haver altres subscrits que no veieu.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importa canal RSS</string>
@@ -291,10 +275,8 @@
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Darrera actualització:</string>
<string name="blogs_rss_remove_feed">Elimina el canal</string>
<string name="blogs_rss_remove_feed_dialog_message">Esteu segurs que voleu eliminar aquest canal i totes les seves publicacions?\nLes publicacions que hàgiu compartit no se suprimiran dels dispositius d\'altres persones.</string>
<string name="blogs_rss_remove_feed_ok">Elimina el canal</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">El canal no s\'ha pogut esborrar.</string>
<string name="blogs_rss_feeds_manage_empty_state">No heu importat cap canal RSS.\n\nPer què no feu clic al botó més de la part superior dreta per afegir el primer?</string>
<string name="blogs_rss_feeds_manage_error">S\'ha produït un problema en carregar els vostres canals. Torneu-ho a provar més tard.</string>
<!--Settings Network-->
<string name="network_settings_title">Xarxes</string>

Some files were not shown because too many files have changed in this diff Show More