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
220 changed files with 9743 additions and 7342 deletions

View File

@@ -1,6 +1,8 @@
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify import de.undercouch.gradle.tasks.download.Verify
import java.security.NoSuchAlgorithmException
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'witness' apply plugin: 'witness'
apply plugin: 'de.undercouch.download' apply plugin: 'de.undercouch.download'
@@ -67,30 +69,68 @@ def torBinaries = [
"geoip" : '8239b98374493529a29096e45fc5877d4d6fdad0146ad8380b291f90d61484ea' "geoip" : '8239b98374493529a29096e45fc5877d4d6fdad0146ad8380b291f90d61484ea'
] ]
def downloadBinary(name) { def verifyOrDeleteBinary(name, chksum, alreadyVerified) {
return tasks.create("downloadBinary${name}", Download) { 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" src "${torDownloadUrl}${name}.zip"
.replace('tor_', "tor-${torVersion}-") .replace('tor_', "tor-${torVersion}-")
.replace('geoip', "geoip-${geoipVersion}") .replace('geoip', "geoip-${geoipVersion}")
.replaceAll('_', '-') .replaceAll('_', '-')
dest "${torBinaryDir}/${name}.zip" dest "${torBinaryDir}/${name}.zip"
onlyIfNewer true onlyIf {
!dest.exists()
}
} }
} }
def verifyBinary(name, chksum) { def verifyBinary(name, chksum) {
boolean[] alreadyVerified = [false]
return tasks.create([ return tasks.create([
name : "verifyBinary${name}", name : "verifyBinary${name}",
type : Verify, type : Verify,
dependsOn: downloadBinary(name)]) { dependsOn: downloadBinary(name, chksum, alreadyVerified)]) {
src "${torBinaryDir}/${name}.zip" src "${torBinaryDir}/${name}.zip"
algorithm 'SHA-256' algorithm 'SHA-256'
checksum chksum checksum chksum
onlyIf {
!alreadyVerified[0]
}
} }
} }
project.afterEvaluate { project.afterEvaluate {
torBinaries.every { key, value -> torBinaries.every { name, checksum ->
preBuild.dependsOn.add(verifyBinary(key, value)) 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, appContext, locationUtils, reporter, eventBus,
torSocketFactory, backoffFactory); torSocketFactory, backoffFactory);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor, DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
backoffFactory, appContext); scheduler, backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex = Collection<DuplexPluginFactory> duplex =
Arrays.asList(bluetooth, tor, lan); Arrays.asList(bluetooth, tor, lan);
@NotNullByDefault @NotNullByDefault

View File

@@ -5,37 +5,84 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; 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.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.net.SocketFactory;
import static android.content.Context.CONNECTIVITY_SERVICE; 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.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
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 @NotNullByDefault
class AndroidLanTcpPlugin extends LanTcpPlugin { class AndroidLanTcpPlugin extends LanTcpPlugin {
// 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 = private static final Logger LOG =
Logger.getLogger(AndroidLanTcpPlugin.class.getName()); 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 Context appContext;
private final ConnectivityManager connectivityManager;
@Nullable
private final WifiManager wifiManager;
@Nullable @Nullable
private volatile BroadcastReceiver networkStateReceiver = null; private volatile BroadcastReceiver networkStateReceiver = null;
private volatile SocketFactory socketFactory;
AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff, AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
Context appContext, DuplexPluginCallback callback, int maxLatency, Backoff backoff, Context appContext, DuplexPluginCallback callback,
int maxIdleTime) { int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime); super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
this.scheduler = scheduler;
this.appContext = appContext; 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 @Override
@@ -44,7 +91,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
running = true; running = true;
// Register to receive network status events // Register to receive network status events
networkStateReceiver = new NetworkStateReceiver(); networkStateReceiver = new NetworkStateReceiver();
IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION); IntentFilter filter = new IntentFilter();
filter.addAction(CONNECTIVITY_ACTION);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
appContext.registerReceiver(networkStateReceiver, filter); appContext.registerReceiver(networkStateReceiver, filter);
} }
@@ -56,21 +105,92 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
tryToClose(socket); 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 { private class NetworkStateReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context ctx, Intent i) { public void onReceive(Context ctx, Intent i) {
if (!running) return; if (!running) return;
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE); if (isApEnabledEvent(i)) {
ConnectivityManager cm = (ConnectivityManager) o; // The state change may be broadcast before the AP address is
NetworkInfo net = cm.getActiveNetworkInfo(); // visible, so delay handling the event
if (net != null && net.getType() == TYPE_WIFI && net.isConnected()) { scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
LOG.info("Connected to Wi-Fi");
if (socket == null || socket.isClosed()) bind();
} else { } else {
LOG.info("Not connected to Wi-Fi"); handleConnectivityChange();
tryToClose(socket);
} }
} }
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 org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -27,12 +28,15 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
private final Context appContext; private final Context appContext;
public AndroidLanTcpPluginFactory(Executor ioExecutor, public AndroidLanTcpPluginFactory(Executor ioExecutor,
BackoffFactory backoffFactory, Context appContext) { ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
Context appContext) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
this.appContext = appContext; this.appContext = appContext;
} }
@@ -51,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) { public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new AndroidLanTcpPlugin(ioExecutor, backoff, appContext, return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
callback, MAX_LATENCY, MAX_IDLE_TIME); 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.EventHandler;
import net.freehaven.tor.control.TorControlConnection; import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.PoliteExecutor;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
@@ -63,8 +64,6 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; 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.logging.Logger;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@@ -111,7 +110,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(TorPlugin.class.getName()); Logger.getLogger(TorPlugin.class.getName());
private final Executor ioExecutor; private final Executor ioExecutor, connectionStatusExecutor;
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final Context appContext; private final Context appContext;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
@@ -125,7 +124,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final File torDirectory, torFile, geoIpFile, configFile; private final File torDirectory, torFile, geoIpFile, configFile;
private final File doneFile, cookieFile; private final File doneFile, cookieFile;
private final PowerManager.WakeLock wakeLock; private final PowerManager.WakeLock wakeLock;
private final Lock connectionStatusLock;
private final AtomicReference<Future<?>> connectivityCheck = private final AtomicReference<Future<?>> connectivityCheck =
new AtomicReference<>(); new AtomicReference<>();
private final AtomicBoolean used = new AtomicBoolean(false); 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. // This tag will prevent Huawei's powermanager from killing us.
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService"); wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
wakeLock.setReferenceCounted(false); 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 @Override
@@ -697,56 +697,46 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
private void updateConnectionStatus() { private void updateConnectionStatus() {
ioExecutor.execute(() -> { connectionStatusExecutor.execute(() -> {
if (!running) return; 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 { try {
connectionStatusLock.lock(); if (!online) {
updateConnectionStatusLocked(); LOG.info("Disabling network, device is offline");
} finally { enableNetwork(false);
connectionStatusLock.unlock(); } 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() { private void scheduleConnectionStatusUpdate() {
Future<?> newConnectivityCheck = Future<?> newConnectivityCheck =
scheduler.schedule(this::updateConnectionStatus, 1, MINUTES); scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);
@@ -788,7 +778,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private synchronized void enableNetwork(boolean enable) { private synchronized void enableNetwork(boolean enable) {
networkEnabled = enable; networkEnabled = enable;
circuitBuilt = false; if (!enable) circuitBuilt = false;
} }
private synchronized boolean isConnected() { private synchronized boolean isConnected() {

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.db.Transaction;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
@@ -88,6 +90,10 @@ public interface ClientHelper {
BdfDictionary toDictionary(byte[] b, int off, int len) BdfDictionary toDictionary(byte[] b, int off, int len)
throws FormatException; 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, int off, int len) throws FormatException;
BdfList toList(byte[] b) throws FormatException; BdfList toList(byte[] b) throws FormatException;
@@ -99,8 +105,15 @@ public interface ClientHelper {
byte[] sign(String label, BdfList toSign, byte[] privateKey) byte[] sign(String label, BdfList toSign, byte[] privateKey)
throws FormatException, GeneralSecurityException; throws FormatException, GeneralSecurityException;
void verifySignature(String label, byte[] sig, byte[] publicKey, void verifySignature(byte[] signature, String label, BdfList signed,
BdfList signed) throws FormatException, GeneralSecurityException; byte[] publicKey) throws FormatException, GeneralSecurityException;
Author parseAndValidateAuthor(BdfList author) throws FormatException; 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 * 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 @Immutable
@NotNullByDefault @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.db.Transaction;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection; import java.util.Collection;
@@ -13,23 +14,28 @@ import java.util.Collection;
public interface ContactManager { 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. * Stores a contact associated with the given local and remote pseudonyms,
*/ * derives and stores transport keys for each transport, and returns an ID
void registerRemoveContactHook(RemoveContactHook hook); * for the contact.
/**
* Stores a contact within the given transaction associated with the given
* local and remote pseudonyms, and returns an ID for the contact.
*/ */
ContactId addContact(Transaction txn, Author remote, AuthorId local, 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; 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, * Stores a contact associated with the given local and remote pseudonyms,
* and returns an ID for the contact. * and returns an ID for the contact.
@@ -94,11 +100,10 @@ public interface ContactManager {
boolean contactExists(AuthorId remoteAuthorId, AuthorId localAuthorId) boolean contactExists(AuthorId remoteAuthorId, AuthorId localAuthorId)
throws DbException; throws DbException;
interface AddContactHook { interface ContactHook {
void addingContact(Transaction txn, Contact c) throws DbException;
} void addingContact(Transaction txn, Contact c) throws DbException;
interface RemoveContactHook {
void removingContact(Transaction txn, Contact c) throws DbException; void removingContact(Transaction txn, Contact c) throws DbException;
} }
} }

View File

@@ -67,8 +67,8 @@ public interface CryptoComponent {
* signature created for another purpose * signature created for another purpose
* @return true if the signature was valid, false otherwise. * @return true if the signature was valid, false otherwise.
*/ */
boolean verify(String label, byte[] signedData, byte[] publicKey, boolean verifySignature(byte[] signature, String label, byte[] signed,
byte[] signature) throws GeneralSecurityException; byte[] publicKey) throws GeneralSecurityException;
/** /**
* Returns the hash of the given inputs. The inputs are unambiguously * 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); 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 * Encrypts and authenticates the given plaintext so it can be written to
* storage. The encryption and authentication keys are derived from the * 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. * The maximum length of a signature in bytes.
*/ */
int MAX_SIGNATURE_BYTES = 64; 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. * rotation period from the given master secret.
* *
* @param alice whether the keys are for use by Alice or Bob. * @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, 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 * 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(); super();
} }
public BdfDictionary(Map<String, Object> m) { public BdfDictionary(Map<String, ?> m) {
super(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.MessageStatus;
import org.briarproject.bramble.api.sync.Offer; import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request; 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 org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection; import java.util.Collection;
@@ -103,10 +104,17 @@ public interface DatabaseComponent {
throws DbException; 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) KeySetId addTransportKeys(Transaction txn, @Nullable ContactId c,
throws DbException; 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 * 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. * Returns the IDs and states of all dependencies of the given message.
* Missing dependencies have the state * For missing dependencies and dependencies in other groups, the state
* {@link ValidationManager.State UNKNOWN}. * {@link State UNKNOWN} is returned.
* 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.
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
@@ -352,9 +356,9 @@ public interface DatabaseComponent {
throws DbException; throws DbException;
/** /**
* Returns all IDs of messages that depend on the given message. * Returns the IDs and states of all dependents of the given message.
* Messages in other groups that declare a dependency on the given message * Dependents in other groups are not returned. If the given message is
* will be returned even though such dependencies are invalid. * missing, no dependents are returned.
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
@@ -399,15 +403,14 @@ public interface DatabaseComponent {
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
Map<ContactId, TransportKeys> getTransportKeys(Transaction txn, Collection<KeySet> getTransportKeys(Transaction txn, TransportId t)
TransportId t) throws DbException; throws DbException;
/** /**
* Increments the outgoing stream counter for the given contact and * Increments the outgoing stream counter for the given transport keys.
* transport in the given rotation period .
*/ */
void incrementStreamCounter(Transaction txn, ContactId c, TransportId t, void incrementStreamCounter(Transaction txn, TransportId t, KeySetId k)
long rotationPeriod) throws DbException; throws DbException;
/** /**
* Merges the given metadata with the existing metadata for the given * 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; 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. * Marks the given contact as verified.
*/ */
@@ -512,15 +521,21 @@ public interface DatabaseComponent {
Collection<MessageId> dependencies) throws DbException; 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. * 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; 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. * Stores the given transport keys, deleting any keys they have replaced.
*/ */
void updateTransportKeys(Transaction txn, void updateTransportKeys(Transaction txn, Collection<KeySet> keys)
Map<ContactId, TransportKeys> keys) throws DbException; throws DbException;
} }

View File

@@ -1,22 +1,23 @@
package org.briarproject.bramble.api.plugin; 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 { 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; private final String id;
public TransportId(String id) { public TransportId(String id) {
byte[] b = id.getBytes(Charset.forName("UTF-8")); int length = StringUtils.toUtf8(id).length;
if (b.length == 0 || b.length > MAX_TRANSPORT_ID_LENGTH) if (length == 0 || length > MAX_TRANSPORT_ID_LENGTH)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
this.id = id; this.id = id;
} }

View File

@@ -1,19 +1,29 @@
package org.briarproject.bramble.api.sync; package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import javax.annotation.concurrent.Immutable; 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 @Immutable
@NotNullByDefault @NotNullByDefault
public class ClientId implements Comparable<ClientId> { 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; private final String id;
public ClientId(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; this.id = id;
} }

View File

@@ -10,6 +10,11 @@ public class Group {
SHARED // The group is visible and messages are shared 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 GroupId id;
private final ClientId clientId; private final ClientId clientId;
private final byte[] descriptor; private final byte[] descriptor;

View File

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

View File

@@ -16,7 +16,13 @@ public class MessageId extends UniqueId {
/** /**
* Label for hashing messages to calculate their identifiers. * 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) { public MessageId(byte[] id) {
super(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.db.Transaction;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
@@ -16,13 +18,51 @@ public interface KeyManager {
/** /**
* Informs the key manager that a new contact has been added. Derives and * 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 * {@link StreamContext StreamContexts} for the contact can be created
* after this method has returned. * after this method has returned.
*/ */
void addContact(Transaction txn, ContactId c, SecretKey master, void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException; 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 * Returns a {@link StreamContext} for sending a stream to the given
* contact over the given transport, or null if an error occurs or the * 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 SecretKey tagKey, headerKey;
private final long rotationPeriod, streamCounter; private final long rotationPeriod, streamCounter;
private final boolean active;
public OutgoingKeys(SecretKey tagKey, SecretKey headerKey, public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
long rotationPeriod) { long rotationPeriod, boolean active) {
this(tagKey, headerKey, rotationPeriod, 0); this(tagKey, headerKey, rotationPeriod, 0, active);
} }
public OutgoingKeys(SecretKey tagKey, SecretKey headerKey, public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
long rotationPeriod, long streamCounter) { long rotationPeriod, long streamCounter, boolean active) {
this.tagKey = tagKey; this.tagKey = tagKey;
this.headerKey = headerKey; this.headerKey = headerKey;
this.rotationPeriod = rotationPeriod; this.rotationPeriod = rotationPeriod;
this.streamCounter = streamCounter; this.streamCounter = streamCounter;
this.active = active;
} }
public SecretKey getTagKey() { public SecretKey getTagKey() {
@@ -39,4 +41,8 @@ public class OutgoingKeys {
public long getStreamCounter() { public long getStreamCounter() {
return streamCounter; return streamCounter;
} }
public boolean isActive() {
return active;
}
} }

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.Author;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor; 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.ClientId;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
@@ -16,13 +18,18 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION; 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_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_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_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; 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.MESSAGE_HEADER_LENGTH;
@@ -54,6 +61,33 @@ public class TestUtils {
return getRandomBytes(UniqueId.LENGTH); 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() { public static SecretKey getSecretKey() {
return new SecretKey(getRandomBytes(SecretKey.LENGTH)); 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.Author;
import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory; 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.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; 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_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.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize; 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 @Override
public BdfList toList(byte[] b, int off, int len) throws FormatException { public BdfList toList(byte[] b, int off, int len) throws FormatException {
ByteArrayInputStream in = new ByteArrayInputStream(b, off, len); ByteArrayInputStream in = new ByteArrayInputStream(b, off, len);
@@ -363,9 +381,10 @@ class ClientHelperImpl implements ClientHelper {
} }
@Override @Override
public void verifySignature(String label, byte[] sig, byte[] publicKey, public void verifySignature(byte[] signature, String label, BdfList signed,
BdfList signed) throws FormatException, GeneralSecurityException { byte[] publicKey) throws FormatException, GeneralSecurityException {
if (!crypto.verify(label, toByteArray(signed), publicKey, sig)) { if (!crypto.verifySignature(signature, label, toByteArray(signed),
publicKey)) {
throw new GeneralSecurityException("Invalid signature"); throw new GeneralSecurityException("Invalid signature");
} }
} }
@@ -382,4 +401,33 @@ class ClientHelperImpl implements ClientHelper {
checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH); checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH);
return authorFactory.createAuthor(formatVersion, name, publicKey); 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(); r.readListEnd();
LOG.info("Received pseudonym"); LOG.info("Received pseudonym");
// Verify the signature // 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)) if (LOG.isLoggable(INFO))
LOG.info("Invalid signature"); LOG.info("Invalid signature");
throw new GeneralSecurityException(); throw new GeneralSecurityException();

View File

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

View File

@@ -205,12 +205,12 @@ class CryptoComponentImpl implements CryptoComponent {
} }
@Override @Override
public boolean verify(String label, byte[] signedData, byte[] publicKey, public boolean verifySignature(byte[] signature, String label,
byte[] signature) throws GeneralSecurityException { byte[] signed, byte[] publicKey) throws GeneralSecurityException {
PublicKey key = signatureKeyParser.parsePublicKey(publicKey); PublicKey key = signatureKeyParser.parsePublicKey(publicKey);
Signature sig = new EdSignature(); Signature sig = new EdSignature();
sig.initVerify(key); sig.initVerify(key);
updateSignature(sig, label, signedData); updateSignature(sig, label, signed);
return sig.verify(signature); return sig.verify(signature);
} }
@@ -262,6 +262,17 @@ class CryptoComponentImpl implements CryptoComponent {
return output; 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 @Override
public byte[] encryptWithPassword(byte[] input, String password) { public byte[] encryptWithPassword(byte[] input, String password) {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher(); AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();

View File

@@ -36,7 +36,8 @@ class TransportCryptoImpl implements TransportCrypto {
@Override @Override
public TransportKeys deriveTransportKeys(TransportId t, 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 // Keys for the previous period are derived from the master secret
SecretKey inTagPrev = deriveTagKey(master, t, !alice); SecretKey inTagPrev = deriveTagKey(master, t, !alice);
SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice); SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice);
@@ -57,7 +58,7 @@ class TransportCryptoImpl implements TransportCrypto {
IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext, IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext,
rotationPeriod + 1); rotationPeriod + 1);
OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr, OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr,
rotationPeriod); rotationPeriod, active);
// Collect and return the keys // Collect and return the keys
return new TransportKeys(t, inPrev, inCurr, inNext, outCurr); return new TransportKeys(t, inPrev, inCurr, inNext, outCurr);
} }
@@ -71,6 +72,7 @@ class TransportCryptoImpl implements TransportCrypto {
IncomingKeys inNext = k.getNextIncomingKeys(); IncomingKeys inNext = k.getNextIncomingKeys();
OutgoingKeys outCurr = k.getCurrentOutgoingKeys(); OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
long startPeriod = outCurr.getRotationPeriod(); long startPeriod = outCurr.getRotationPeriod();
boolean active = outCurr.isActive();
// Rotate the keys // Rotate the keys
for (long p = startPeriod + 1; p <= rotationPeriod; p++) { for (long p = startPeriod + 1; p <= rotationPeriod; p++) {
inPrev = inCurr; inPrev = inCurr;
@@ -80,7 +82,7 @@ class TransportCryptoImpl implements TransportCrypto {
inNext = new IncomingKeys(inNextTag, inNextHeader, p + 1); inNext = new IncomingKeys(inNextTag, inNextHeader, p + 1);
SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p); SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p);
SecretKey outCurrHeader = rotateKey(outCurr.getHeaderKey(), 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 // Collect and return the keys
return new TransportKeys(k.getTransportId(), inPrev, inCurr, inNext, 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.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.ValidationManager.State; 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 org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection; import java.util.Collection;
@@ -105,10 +107,11 @@ interface Database<T> {
@Nullable ContactId sender) throws DbException; @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, void addMessageDependency(T txn, Message dependent, MessageId dependency,
MessageId dependency) throws DbException; State dependentState) throws DbException;
/** /**
* Records that a message has been offered by the given contact. * Records that a message has been offered by the given contact.
@@ -122,9 +125,16 @@ interface Database<T> {
throws DbException; 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; throws DbException;
/** /**
@@ -292,10 +302,8 @@ interface Database<T> {
/** /**
* Returns the IDs and states of all dependencies of the given message. * Returns the IDs and states of all dependencies of the given message.
* Missing dependencies have the state {@link State UNKNOWN}. * For missing dependencies and dependencies in other groups, the state
* Dependencies in other groups have the state {@link State INVALID}. * {@link State UNKNOWN} is returned.
* 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.
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
@@ -303,9 +311,9 @@ interface Database<T> {
throws DbException; throws DbException;
/** /**
* Returns all IDs and states of all dependents of the given message. * Returns the IDs and states of all dependents of the given message.
* Messages in other groups that declare a dependency on the given message * Dependents in other groups are not returned. If the given message is
* will be returned even though such dependencies are invalid. * missing, no dependents are returned.
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
@@ -487,15 +495,14 @@ interface Database<T> {
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
Map<ContactId, TransportKeys> getTransportKeys(T txn, TransportId t) Collection<KeySet> getTransportKeys(T txn, TransportId t)
throws DbException; throws DbException;
/** /**
* Increments the outgoing stream counter for the given contact and * Increments the outgoing stream counter for the given transport keys.
* transport in the given rotation period.
*/ */
void incrementStreamCounter(T txn, ContactId c, TransportId t, void incrementStreamCounter(T txn, TransportId t, KeySetId k)
long rotationPeriod) throws DbException; throws DbException;
/** /**
* Marks the given messages as not needing to be acknowledged to the * 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; 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 * Resets the transmission count and expiry time of the given message with
* respect to the given contact. * respect to the given contact.
@@ -620,12 +633,18 @@ interface Database<T> {
void setMessageState(T txn, MessageId m, State state) throws DbException; 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. * 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; 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 * Updates the transmission count and expiry time of the given message
* with respect to the given contact, using the latency of the transport * 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. * Stores the given transport keys, deleting any keys they have replaced.
*/ */
void updateTransportKeys(T txn, Map<ContactId, TransportKeys> keys) void updateTransportKeys(T txn, Collection<KeySet> keys) throws DbException;
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.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; 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 org.briarproject.bramble.api.transport.TransportKeys;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -234,15 +234,27 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
@Override @Override
public void addTransportKeys(Transaction transaction, ContactId c, public KeySetId addTransportKeys(Transaction transaction,
TransportKeys k) throws DbException { @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(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
if (!db.containsTransport(txn, k.getTransportId())) if (!db.containsTransport(txn, t))
throw new NoSuchTransportException(); throw new NoSuchTransportException();
db.addTransportKeys(txn, c, k); db.bindTransportKeys(txn, c, t, k);
} }
@Override @Override
@@ -586,8 +598,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
@Override @Override
public Map<ContactId, TransportKeys> getTransportKeys( public Collection<KeySet> getTransportKeys(Transaction transaction,
Transaction transaction, TransportId t) throws DbException { TransportId t) throws DbException {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsTransport(txn, t)) if (!db.containsTransport(txn, t))
throw new NoSuchTransportException(); throw new NoSuchTransportException();
@@ -595,15 +607,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
@Override @Override
public void incrementStreamCounter(Transaction transaction, ContactId c, public void incrementStreamCounter(Transaction transaction, TransportId t,
TransportId t, long rotationPeriod) throws DbException { KeySetId k) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, t)) if (!db.containsTransport(txn, t))
throw new NoSuchTransportException(); throw new NoSuchTransportException();
db.incrementStreamCounter(txn, c, t, rotationPeriod); db.incrementStreamCounter(txn, t, k);
} }
@Override @Override
@@ -765,6 +775,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsMessage(txn, m)) if (!db.containsMessage(txn, m))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
// TODO: Don't allow messages with dependents to be removed
db.removeMessage(txn, m); db.removeMessage(txn, m);
} }
@@ -778,6 +789,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.removeTransport(txn, t); 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 @Override
public void setContactVerified(Transaction transaction, ContactId c) public void setContactVerified(Transaction transaction, ContactId c)
throws DbException { throws DbException {
@@ -850,38 +871,42 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsMessage(txn, dependent.getId())) if (!db.containsMessage(txn, dependent.getId()))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
State dependentState = db.getMessageState(txn, dependent.getId());
for (MessageId dependency : dependencies) { for (MessageId dependency : dependencies) {
db.addMessageDependency(txn, dependent.getGroupId(), db.addMessageDependency(txn, dependent, dependency, dependentState);
dependent.getId(), dependency);
} }
} }
@Override @Override
public void setReorderingWindow(Transaction transaction, ContactId c, public void setReorderingWindow(Transaction transaction, KeySetId k,
TransportId t, long rotationPeriod, long base, byte[] bitmap) TransportId t, long rotationPeriod, long base, byte[] bitmap)
throws DbException { throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
if (!db.containsTransport(txn, t)) if (!db.containsTransport(txn, t))
throw new NoSuchTransportException(); 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 @Override
public void updateTransportKeys(Transaction transaction, public void updateTransportKeys(Transaction transaction,
Map<ContactId, TransportKeys> keys) throws DbException { Collection<KeySet> keys) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
Map<ContactId, TransportKeys> filtered = new HashMap<>(); Collection<KeySet> filtered = new ArrayList<>();
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) { for (KeySet ks : keys) {
ContactId c = e.getKey(); TransportId t = ks.getTransportKeys().getTransportId();
TransportKeys k = e.getValue(); if (db.containsTransport(txn, t)) filtered.add(ks);
if (db.containsContact(txn, c)
&& db.containsTransport(txn, k.getTransportId())) {
filtered.put(c, k);
}
} }
db.updateTransportKeys(txn, filtered); 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.sync.ValidationManager.State;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys; 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.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
@@ -50,6 +52,7 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.sql.Types.INTEGER;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.db.Metadata.REMOVE; 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.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; 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.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.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE; 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> { abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing // Package access for testing
static final int CODE_SCHEMA_VERSION = 35; static final int CODE_SCHEMA_VERSION = 36;
private static final String CREATE_SETTINGS = private static final String CREATE_SETTINGS =
"CREATE TABLE settings" "CREATE TABLE settings"
@@ -170,6 +172,10 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " (groupId _HASH NOT NULL," + " (groupId _HASH NOT NULL,"
+ " messageId _HASH NOT NULL," + " messageId _HASH NOT NULL,"
+ " dependencyId _HASH NOT NULL," // Not a foreign key + " 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)" + " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)" + " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE," + " ON DELETE CASCADE,"
@@ -219,37 +225,44 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " maxLatency INT NOT NULL," + " maxLatency INT NOT NULL,"
+ " PRIMARY KEY (transportId))"; + " 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 = private static final String CREATE_INCOMING_KEYS =
"CREATE TABLE incomingKeys" "CREATE TABLE incomingKeys"
+ " (contactId INT NOT NULL," + " (transportId _STRING NOT NULL,"
+ " transportId _STRING NOT NULL," + " keySetId INT NOT NULL,"
+ " rotationPeriod BIGINT NOT NULL," + " rotationPeriod BIGINT NOT NULL,"
+ " contactId INT," // Null if keys are not bound
+ " tagKey _SECRET NOT NULL," + " tagKey _SECRET NOT NULL,"
+ " headerKey _SECRET NOT NULL," + " headerKey _SECRET NOT NULL,"
+ " base BIGINT NOT NULL," + " base BIGINT NOT NULL,"
+ " bitmap _BINARY NOT NULL," + " bitmap _BINARY NOT NULL,"
+ " PRIMARY KEY (contactId, transportId, rotationPeriod)," + " PRIMARY KEY (transportId, keySetId, rotationPeriod),"
+ " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (transportId)" + " FOREIGN KEY (transportId)"
+ " REFERENCES transports (transportId)" + " REFERENCES transports (transportId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE,"
+ " FOREIGN KEY (keySetId)"
private static final String CREATE_OUTGOING_KEYS = + " REFERENCES outgoingKeys (keySetId)"
"CREATE TABLE outgoingKeys" + " ON DELETE CASCADE,"
+ " (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),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)" + " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (transportId)"
+ " REFERENCES transports (transportId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String INDEX_CONTACTS_BY_AUTHOR_ID = 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" "CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
+ " ON messageMetadata (groupId, state)"; + " 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 = private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID =
"CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId" "CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId"
+ " ON statuses (contactId, groupId)"; + " ON statuses (contactId, groupId)";
@@ -407,8 +424,8 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(insertTypeNames(CREATE_OFFERS)); s.executeUpdate(insertTypeNames(CREATE_OFFERS));
s.executeUpdate(insertTypeNames(CREATE_STATUSES)); s.executeUpdate(insertTypeNames(CREATE_STATUSES));
s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
s.executeUpdate(insertTypeNames(CREATE_INCOMING_KEYS));
s.executeUpdate(insertTypeNames(CREATE_OUTGOING_KEYS)); s.executeUpdate(insertTypeNames(CREATE_OUTGOING_KEYS));
s.executeUpdate(insertTypeNames(CREATE_INCOMING_KEYS));
s.close(); s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s); tryToClose(s);
@@ -423,6 +440,7 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID); s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID); s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE); 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_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP); s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
s.close(); s.close();
@@ -715,6 +733,17 @@ abstract class JdbcDatabase implements Database<Connection> {
m.getLength(), state, e.getValue(), messageShared, m.getLength(), state, e.getValue(), messageShared,
false, seen); 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) { } catch (SQLException e) {
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);
@@ -784,21 +813,42 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void addMessageDependency(Connection txn, GroupId g, public void addMessageDependency(Connection txn, Message dependent,
MessageId dependent, MessageId dependency) throws DbException { MessageId dependency, State dependentState) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null;
try { try {
String sql = "INSERT INTO messageDependencies" // Get state of dependency if present and in same group as dependent
+ " (groupId, messageId, dependencyId)" String sql = "SELECT state FROM messages"
+ " VALUES (?, ?, ?)"; + " WHERE messageId = ? AND groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes()); ps.setBytes(1, dependency.getBytes());
ps.setBytes(2, dependent.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.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(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); ps.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs);
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);
} }
@@ -824,61 +874,105 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void addTransportKeys(Connection txn, ContactId c, TransportKeys k) public KeySetId addTransportKeys(Connection txn, @Nullable ContactId c,
throws DbException { TransportKeys k) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null;
try { try {
// Store the incoming keys // Store the outgoing keys
String sql = "INSERT INTO incomingKeys (contactId, transportId," String sql = "INSERT INTO outgoingKeys (contactId, transportId,"
+ " rotationPeriod, tagKey, headerKey, base, bitmap)" + " rotationPeriod, tagKey, headerKey, stream, active)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); if (c == null) ps.setNull(1, INTEGER);
ps.setString(2, k.getTransportId().getString()); else ps.setInt(1, c.getInt());
// 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());
ps.setString(2, k.getTransportId().getString()); ps.setString(2, k.getTransportId().getString());
OutgoingKeys outCurr = k.getCurrentOutgoingKeys(); OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
ps.setLong(3, outCurr.getRotationPeriod()); ps.setLong(3, outCurr.getRotationPeriod());
ps.setBytes(4, outCurr.getTagKey().getBytes()); ps.setBytes(4, outCurr.getTagKey().getBytes());
ps.setBytes(5, outCurr.getHeaderKey().getBytes()); ps.setBytes(5, outCurr.getHeaderKey().getBytes());
ps.setLong(6, outCurr.getStreamCounter()); ps.setLong(6, outCurr.getStreamCounter());
ps.setBoolean(7, outCurr.isActive());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); 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) { } catch (SQLException e) {
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);
@@ -1663,11 +1757,9 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT d.dependencyId, m.state, d.groupId, m.groupId" String sql = "SELECT dependencyId, dependencyState"
+ " FROM messageDependencies AS d" + " FROM messageDependencies"
+ " LEFT OUTER JOIN messages AS m" + " WHERE messageId = ?";
+ " ON d.dependencyId = m.messageId"
+ " WHERE d.messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -1675,14 +1767,8 @@ abstract class JdbcDatabase implements Database<Connection> {
while (rs.next()) { while (rs.next()) {
MessageId dependency = new MessageId(rs.getBytes(1)); MessageId dependency = new MessageId(rs.getBytes(1));
State state = State.fromValue(rs.getInt(2)); State state = State.fromValue(rs.getInt(2));
if (rs.wasNull()) { if (rs.wasNull())
state = UNKNOWN; // Missing dependency state = UNKNOWN; // Missing or in a different group
} 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
}
dependencies.put(dependency, state); dependencies.put(dependency, state);
} }
rs.close(); rs.close();
@@ -1701,11 +1787,12 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT d.messageId, m.state" // Exclude dependencies that are missing or in a different group
+ " FROM messageDependencies AS d" // from the dependent
+ " JOIN messages AS m" String sql = "SELECT messageId, messageState"
+ " ON d.messageId = m.messageId" + " FROM messageDependencies"
+ " WHERE dependencyId = ?"; + " WHERE dependencyId = ?"
+ " AND dependencyState IS NOT NULL";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -2044,8 +2131,8 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public Map<ContactId, TransportKeys> getTransportKeys(Connection txn, public Collection<KeySet> getTransportKeys(Connection txn, TransportId t)
TransportId t) throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
@@ -2054,7 +2141,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " base, bitmap" + " base, bitmap"
+ " FROM incomingKeys" + " FROM incomingKeys"
+ " WHERE transportId = ?" + " WHERE transportId = ?"
+ " ORDER BY contactId, rotationPeriod"; + " ORDER BY keySetId, rotationPeriod";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, t.getString()); ps.setString(1, t.getString());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -2071,29 +2158,34 @@ abstract class JdbcDatabase implements Database<Connection> {
rs.close(); rs.close();
ps.close(); ps.close();
// Retrieve the outgoing keys in the same order // 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" + " FROM outgoingKeys"
+ " WHERE transportId = ?" + " WHERE transportId = ?"
+ " ORDER BY contactId, rotationPeriod"; + " ORDER BY keySetId";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, t.getString()); ps.setString(1, t.getString());
rs = ps.executeQuery(); rs = ps.executeQuery();
Map<ContactId, TransportKeys> keys = new HashMap<>(); Collection<KeySet> keys = new ArrayList<>();
for (int i = 0; rs.next(); i++) { for (int i = 0; rs.next(); i++) {
// There should be three times as many incoming keys // There should be three times as many incoming keys
if (inKeys.size() < (i + 1) * 3) throw new DbStateException(); if (inKeys.size() < (i + 1) * 3) throw new DbStateException();
ContactId contactId = new ContactId(rs.getInt(1)); KeySetId keySetId = new KeySetId(rs.getInt(1));
long rotationPeriod = rs.getLong(2); ContactId contactId = new ContactId(rs.getInt(2));
SecretKey tagKey = new SecretKey(rs.getBytes(3)); if (rs.wasNull()) contactId = null;
SecretKey headerKey = new SecretKey(rs.getBytes(4)); long rotationPeriod = rs.getLong(3);
long streamCounter = rs.getLong(5); 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, OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
rotationPeriod, streamCounter); rotationPeriod, streamCounter, active);
IncomingKeys inPrev = inKeys.get(i * 3); IncomingKeys inPrev = inKeys.get(i * 3);
IncomingKeys inCurr = inKeys.get(i * 3 + 1); IncomingKeys inCurr = inKeys.get(i * 3 + 1);
IncomingKeys inNext = inKeys.get(i * 3 + 2); IncomingKeys inNext = inKeys.get(i * 3 + 2);
keys.put(contactId, new TransportKeys(t, inPrev, inCurr, TransportKeys transportKeys = new TransportKeys(t, inPrev,
inNext, outCurr)); inCurr, inNext, outCurr);
keys.add(new KeySet(keySetId, contactId, transportKeys));
} }
rs.close(); rs.close();
ps.close(); ps.close();
@@ -2106,17 +2198,15 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void incrementStreamCounter(Connection txn, ContactId c, public void incrementStreamCounter(Connection txn, TransportId t,
TransportId t, long rotationPeriod) throws DbException { KeySetId k) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE outgoingKeys SET stream = stream + 1" String sql = "UPDATE outgoingKeys SET stream = stream + 1"
+ " WHERE contactId = ? AND transportId = ?" + " WHERE transportId = ? AND keySetId = ?";
+ " AND rotationPeriod = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setString(1, t.getString());
ps.setString(2, t.getString()); ps.setInt(2, k.getInt());
ps.setLong(3, rotationPeriod);
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); 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 @Override
public void resetExpiryTime(Connection txn, ContactId c, MessageId m) public void resetExpiryTime(Connection txn, ContactId c, MessageId m)
throws DbException { throws DbException {
@@ -2731,6 +2842,25 @@ abstract class JdbcDatabase implements Database<Connection> {
affected = ps.executeUpdate(); affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException(); if (affected < 0) throw new DbStateException();
ps.close(); 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) { } catch (SQLException e) {
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);
@@ -2738,18 +2868,18 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @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 { long rotationPeriod, long base, byte[] bitmap) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?" String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?"
+ " WHERE contactId = ? AND transportId = ?" + " WHERE transportId = ? AND keySetId = ?"
+ " AND rotationPeriod = ?"; + " AND rotationPeriod = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setLong(1, base); ps.setLong(1, base);
ps.setBytes(2, bitmap); ps.setBytes(2, bitmap);
ps.setInt(3, c.getInt()); ps.setString(3, t.getString());
ps.setString(4, t.getString()); ps.setInt(4, k.getInt());
ps.setLong(5, rotationPeriod); ps.setLong(5, rotationPeriod);
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException(); 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 @Override
public void updateExpiryTime(Connection txn, ContactId c, MessageId m, public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException { int maxLatency) throws DbException {
@@ -2795,45 +2944,12 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void updateTransportKeys(Connection txn, public void updateTransportKeys(Connection txn, Collection<KeySet> keys)
Map<ContactId, TransportKeys> keys) throws DbException { throws DbException {
PreparedStatement ps = null; for (KeySet ks : keys) {
try { TransportKeys k = ks.getTransportKeys();
// Delete any existing incoming keys removeTransportKeys(txn, k.getTransportId(), ks.getKeySetId());
String sql = "DELETE FROM incomingKeys" addTransportKeys(txn, ks.getContactId(), k);
+ " 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());
} }
} }
} }

View File

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

View File

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

View File

@@ -46,8 +46,7 @@ public class PropertiesModule {
lifecycleManager.registerClient(transportPropertyManager); lifecycleManager.registerClient(transportPropertyManager);
validationManager.registerIncomingMessageHook(CLIENT_ID, validationManager.registerIncomingMessageHook(CLIENT_ID,
transportPropertyManager); transportPropertyManager);
contactManager.registerAddContactHook(transportPropertyManager); contactManager.registerContactHook(transportPropertyManager);
contactManager.registerRemoveContactHook(transportPropertyManager);
return 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.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.AddContactHook; import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.contact.ContactManager.RemoveContactHook;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.data.MetadataParser;
@@ -40,7 +39,7 @@ import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class TransportPropertyManagerImpl implements TransportPropertyManager, class TransportPropertyManagerImpl implements TransportPropertyManager,
Client, AddContactHook, RemoveContactHook, IncomingMessageHook { Client, ContactHook, IncomingMessageHook {
private final DatabaseComponent db; private final DatabaseComponent db;
private final ClientHelper clientHelper; private final ClientHelper clientHelper;
@@ -348,10 +347,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
throws FormatException { throws FormatException {
// Transport ID, version, properties // Transport ID, version, properties
BdfDictionary dictionary = message.getDictionary(2); BdfDictionary dictionary = message.getDictionary(2);
TransportProperties p = new TransportProperties(); return clientHelper.parseAndValidateTransportProperties(dictionary);
for (String key : dictionary.keySet())
p.put(key, dictionary.getString(key));
return p;
} }
private static class LatestUpdate { private static class LatestUpdate {

View File

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

View File

@@ -12,8 +12,8 @@ import org.briarproject.bramble.util.StringUtils;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; 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.GroupId.LABEL;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES; import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
@Immutable @Immutable
@@ -31,7 +31,7 @@ class GroupFactoryImpl implements GroupFactory {
public Group createGroup(ClientId c, int clientVersion, byte[] descriptor) { public Group createGroup(ClientId c, int clientVersion, byte[] descriptor) {
byte[] clientVersionBytes = new byte[INT_32_BYTES]; byte[] clientVersionBytes = new byte[INT_32_BYTES];
ByteUtils.writeUint32(clientVersion, clientVersionBytes, 0); 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, StringUtils.toUtf8(c.getString()), clientVersionBytes,
descriptor); descriptor);
return new Group(new GroupId(hash), c, 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.annotation.concurrent.Immutable;
import javax.inject.Inject; 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.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_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 @Immutable
@NotNullByDefault @NotNullByDefault
@@ -32,11 +34,14 @@ class MessageFactoryImpl implements MessageFactory {
public Message createMessage(GroupId g, long timestamp, byte[] body) { public Message createMessage(GroupId g, long timestamp, byte[] body) {
if (body.length > MAX_MESSAGE_BODY_LENGTH) if (body.length > MAX_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException(); 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); ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] hash = crypto.hash(LABEL, new byte[] {PROTOCOL_VERSION}, byte[] idHash = crypto.hash(ID_LABEL, versionBytes, g.getBytes(),
g.getBytes(), timeBytes, body); timeBytes, rootHash);
MessageId id = new MessageId(hash); MessageId id = new MessageId(idHash);
byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length]; byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length];
System.arraycopy(g.getBytes(), 0, raw, 0, UniqueId.LENGTH); System.arraycopy(g.getBytes(), 0, raw, 0, UniqueId.LENGTH);
ByteUtils.writeUint64(timestamp, raw, 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.Clock;
import org.briarproject.bramble.api.system.Scheduler; 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.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -25,7 +26,10 @@ public class SystemModule {
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
public SystemModule() { public SystemModule() {
scheduler = Executors.newSingleThreadScheduledExecutor(); // Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ScheduledThreadPoolExecutor.DiscardPolicy();
scheduler = new ScheduledThreadPoolExecutor(1, policy);
} }
@Provides @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.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext; import org.briarproject.bramble.api.transport.StreamContext;
import java.util.HashMap; import java.util.HashMap;
@@ -104,6 +105,67 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
m.addContact(txn, c, master, timestamp, alice); 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 @Override
public StreamContext getStreamContext(ContactId c, TransportId t) public StreamContext getStreamContext(ContactId c, TransportId t)
throws DbException { throws DbException {
@@ -114,7 +176,7 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t); if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
return null; return null;
} }
StreamContext ctx = null; StreamContext ctx;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
ctx = m.getStreamContext(txn, c); 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); if (LOG.isLoggable(INFO)) LOG.info("No key manager for " + t);
return null; return null;
} }
StreamContext ctx = null; StreamContext ctx;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
ctx = m.getStreamContext(txn, tag); 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 SecretKey tagKey, headerKey;
private final long rotationPeriod; private final long rotationPeriod;
private long streamCounter; private long streamCounter;
private boolean active;
MutableOutgoingKeys(OutgoingKeys out) { MutableOutgoingKeys(OutgoingKeys out) {
tagKey = out.getTagKey(); tagKey = out.getTagKey();
headerKey = out.getHeaderKey(); headerKey = out.getHeaderKey();
rotationPeriod = out.getRotationPeriod(); rotationPeriod = out.getRotationPeriod();
streamCounter = out.getStreamCounter(); streamCounter = out.getStreamCounter();
active = out.isActive();
} }
OutgoingKeys snapshot() { OutgoingKeys snapshot() {
return new OutgoingKeys(tagKey, headerKey, rotationPeriod, return new OutgoingKeys(tagKey, headerKey, rotationPeriod,
streamCounter); streamCounter, active);
} }
SecretKey getTagKey() { SecretKey getTagKey() {
@@ -45,4 +47,12 @@ class MutableOutgoingKeys {
void incrementStreamCounter() { void incrementStreamCounter() {
streamCounter++; 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.DbException;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.StreamContext; import org.briarproject.bramble.api.transport.StreamContext;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -17,8 +18,19 @@ interface TransportKeyManager {
void addContact(Transaction txn, ContactId c, SecretKey master, void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException; 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); void removeContact(ContactId c);
boolean canSendOutgoingStreams(ContactId c);
@Nullable @Nullable
StreamContext getStreamContext(Transaction txn, ContactId c) StreamContext getStreamContext(Transaction txn, ContactId c)
throws DbException; 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.plugin.TransportId;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler; 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.StreamContext;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.transport.ReorderingWindow.Change; import org.briarproject.bramble.transport.ReorderingWindow.Change;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -47,12 +52,13 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private final Clock clock; private final Clock clock;
private final TransportId transportId; private final TransportId transportId;
private final long rotationPeriodLength; 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 // The following are locking: lock
private final Map<Bytes, TagContext> inContexts; private final Map<KeySetId, MutableKeySet> keys = new HashMap<>();
private final Map<ContactId, MutableOutgoingKeys> outContexts; private final Map<Bytes, TagContext> inContexts = new HashMap<>();
private final Map<ContactId, MutableTransportKeys> keys; private final Map<ContactId, MutableKeySet> outContexts = new HashMap<>();
TransportKeyManagerImpl(DatabaseComponent db, TransportKeyManagerImpl(DatabaseComponent db,
TransportCrypto transportCrypto, Executor dbExecutor, TransportCrypto transportCrypto, Executor dbExecutor,
@@ -65,20 +71,16 @@ class TransportKeyManagerImpl implements TransportKeyManager {
this.clock = clock; this.clock = clock;
this.transportId = transportId; this.transportId = transportId;
rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE; rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
lock = new ReentrantLock();
inContexts = new HashMap<>();
outContexts = new HashMap<>();
keys = new HashMap<>();
} }
@Override @Override
public void start(Transaction txn) throws DbException { public void start(Transaction txn) throws DbException {
if (used.getAndSet(true)) throw new IllegalStateException();
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
lock.lock(); lock.lock();
try { try {
// Load the transport keys from the DB // Load the transport keys from the DB
Map<ContactId, TransportKeys> loaded = Collection<KeySet> loaded = db.getTransportKeys(txn, transportId);
db.getTransportKeys(txn, transportId);
// Rotate the keys to the current rotation period // Rotate the keys to the current rotation period
RotationResult rotationResult = rotateKeys(loaded, now); RotationResult rotationResult = rotateKeys(loaded, now);
// Initialise mutable state for all contacts // Initialise mutable state for all contacts
@@ -93,41 +95,48 @@ class TransportKeyManagerImpl implements TransportKeyManager {
scheduleKeyRotation(now); scheduleKeyRotation(now);
} }
private RotationResult rotateKeys(Map<ContactId, TransportKeys> keys, private RotationResult rotateKeys(Collection<KeySet> keys, long now) {
long now) {
RotationResult rotationResult = new RotationResult(); RotationResult rotationResult = new RotationResult();
long rotationPeriod = now / rotationPeriodLength; long rotationPeriod = now / rotationPeriodLength;
for (Entry<ContactId, TransportKeys> e : keys.entrySet()) { for (KeySet ks : keys) {
ContactId c = e.getKey(); TransportKeys k = ks.getTransportKeys();
TransportKeys k = e.getValue();
TransportKeys k1 = TransportKeys k1 =
transportCrypto.rotateTransportKeys(k, rotationPeriod); transportCrypto.rotateTransportKeys(k, rotationPeriod);
KeySet ks1 = new KeySet(ks.getKeySetId(), ks.getContactId(), k1);
if (k1.getRotationPeriod() > k.getRotationPeriod()) if (k1.getRotationPeriod() > k.getRotationPeriod())
rotationResult.rotated.put(c, k1); rotationResult.rotated.add(ks1);
rotationResult.current.put(c, k1); rotationResult.current.add(ks1);
} }
return rotationResult; return rotationResult;
} }
// Locking: lock // Locking: lock
private void addKeys(Map<ContactId, TransportKeys> m) { private void addKeys(Collection<KeySet> keys) {
for (Entry<ContactId, TransportKeys> e : m.entrySet()) for (KeySet ks : keys) {
addKeys(e.getKey(), new MutableTransportKeys(e.getValue())); addKeys(ks.getKeySetId(), ks.getContactId(),
new MutableTransportKeys(ks.getTransportKeys()));
}
} }
// Locking: lock // Locking: lock
private void addKeys(ContactId c, MutableTransportKeys m) { private void addKeys(KeySetId keySetId, @Nullable ContactId contactId,
encodeTags(c, m.getPreviousIncomingKeys()); MutableTransportKeys m) {
encodeTags(c, m.getCurrentIncomingKeys()); MutableKeySet ks = new MutableKeySet(keySetId, contactId, m);
encodeTags(c, m.getNextIncomingKeys()); keys.put(keySetId, ks);
outContexts.put(c, m.getCurrentOutgoingKeys()); if (contactId != null) {
keys.put(c, m); encodeTags(keySetId, contactId, m.getPreviousIncomingKeys());
encodeTags(keySetId, contactId, m.getCurrentIncomingKeys());
encodeTags(keySetId, contactId, m.getNextIncomingKeys());
considerReplacingOutgoingKeys(ks);
}
} }
// Locking: lock // Locking: lock
private void encodeTags(ContactId c, MutableIncomingKeys inKeys) { private void encodeTags(KeySetId keySetId, ContactId contactId,
MutableIncomingKeys inKeys) {
for (long streamNumber : inKeys.getWindow().getUnseen()) { 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]; byte[] tag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(tag, inKeys.getTagKey(), PROTOCOL_VERSION, transportCrypto.encodeTag(tag, inKeys.getTagKey(), PROTOCOL_VERSION,
streamNumber); 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) { private void scheduleKeyRotation(long now) {
long delay = rotationPeriodLength - now % rotationPeriodLength; long delay = rotationPeriodLength - now % rotationPeriodLength;
scheduler.schedule((Runnable) this::rotateKeys, delay, MILLISECONDS); scheduler.schedule((Runnable) this::rotateKeys, delay, MILLISECONDS);
@@ -159,20 +179,82 @@ class TransportKeyManagerImpl implements TransportKeyManager {
@Override @Override
public void addContact(Transaction txn, ContactId c, SecretKey master, public void addContact(Transaction txn, ContactId c, SecretKey master,
long timestamp, boolean alice) throws DbException { 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(); lock.lock();
try { try {
// Work out what rotation period the timestamp belongs to // Work out what rotation period the timestamp belongs to
long rotationPeriod = timestamp / rotationPeriodLength; long rotationPeriod = timestamp / rotationPeriodLength;
// Derive the transport keys // Derive the transport keys
TransportKeys k = transportCrypto.deriveTransportKeys(transportId, TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
master, rotationPeriod, alice); master, rotationPeriod, alice, active);
// Rotate the keys to the current rotation period if necessary // Rotate the keys to the current rotation period if necessary
rotationPeriod = clock.currentTimeMillis() / rotationPeriodLength; rotationPeriod = clock.currentTimeMillis() / rotationPeriodLength;
k = transportCrypto.rotateTransportKeys(k, rotationPeriod); k = transportCrypto.rotateTransportKeys(k, rotationPeriod);
// Initialise mutable state for the contact
addKeys(c, new MutableTransportKeys(k));
// Write the keys back to the DB // 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 { } finally {
lock.unlock(); lock.unlock();
} }
@@ -183,12 +265,29 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock(); lock.lock();
try { try {
// Remove mutable state for the contact // Remove mutable state for the contact
Iterator<Entry<Bytes, TagContext>> it = Iterator<TagContext> it = inContexts.values().iterator();
inContexts.entrySet().iterator(); while (it.hasNext()) if (it.next().contactId.equals(c)) it.remove();
while (it.hasNext())
if (it.next().getValue().contactId.equals(c)) it.remove();
outContexts.remove(c); 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 { } finally {
lock.unlock(); lock.unlock();
} }
@@ -200,8 +299,11 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock(); lock.lock();
try { try {
// Look up the outgoing keys for the contact // Look up the outgoing keys for the contact
MutableOutgoingKeys outKeys = outContexts.get(c); MutableKeySet ks = outContexts.get(c);
if (outKeys == null) return null; 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; if (outKeys.getStreamCounter() > MAX_32_BIT_UNSIGNED) return null;
// Create a stream context // Create a stream context
StreamContext ctx = new StreamContext(c, transportId, StreamContext ctx = new StreamContext(c, transportId,
@@ -209,8 +311,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
outKeys.getStreamCounter()); outKeys.getStreamCounter());
// Increment the stream counter and write it back to the DB // Increment the stream counter and write it back to the DB
outKeys.incrementStreamCounter(); outKeys.incrementStreamCounter();
db.incrementStreamCounter(txn, c, transportId, db.incrementStreamCounter(txn, transportId, ks.getKeySetId());
outKeys.getRotationPeriod());
return ctx; return ctx;
} finally { } finally {
lock.unlock(); lock.unlock();
@@ -238,8 +339,9 @@ class TransportKeyManagerImpl implements TransportKeyManager {
byte[] addTag = new byte[TAG_LENGTH]; byte[] addTag = new byte[TAG_LENGTH];
transportCrypto.encodeTag(addTag, inKeys.getTagKey(), transportCrypto.encodeTag(addTag, inKeys.getTagKey(),
PROTOCOL_VERSION, streamNumber); PROTOCOL_VERSION, streamNumber);
inContexts.put(new Bytes(addTag), new TagContext( TagContext tagCtx1 = new TagContext(tagCtx.keySetId,
tagCtx.contactId, inKeys, streamNumber)); tagCtx.contactId, inKeys, streamNumber);
inContexts.put(new Bytes(addTag), tagCtx1);
} }
// Remove tags for any stream numbers removed from the window // Remove tags for any stream numbers removed from the window
for (long streamNumber : change.getRemoved()) { for (long streamNumber : change.getRemoved()) {
@@ -250,9 +352,19 @@ class TransportKeyManagerImpl implements TransportKeyManager {
inContexts.remove(new Bytes(removeTag)); inContexts.remove(new Bytes(removeTag));
} }
// Write the window back to the DB // Write the window back to the DB
db.setReorderingWindow(txn, tagCtx.contactId, transportId, db.setReorderingWindow(txn, tagCtx.keySetId, transportId,
inKeys.getRotationPeriod(), window.getBase(), inKeys.getRotationPeriod(), window.getBase(),
window.getBitmap()); 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; return ctx;
} finally { } finally {
lock.unlock(); lock.unlock();
@@ -264,9 +376,11 @@ class TransportKeyManagerImpl implements TransportKeyManager {
lock.lock(); lock.lock();
try { try {
// Rotate the keys to the current rotation period // Rotate the keys to the current rotation period
Map<ContactId, TransportKeys> snapshot = new HashMap<>(); Collection<KeySet> snapshot = new ArrayList<>(keys.size());
for (Entry<ContactId, MutableTransportKeys> e : keys.entrySet()) for (MutableKeySet ks : keys.values()) {
snapshot.put(e.getKey(), e.getValue().snapshot()); snapshot.add(new KeySet(ks.getKeySetId(), ks.getContactId(),
ks.getTransportKeys().snapshot()));
}
RotationResult rotationResult = rotateKeys(snapshot, now); RotationResult rotationResult = rotateKeys(snapshot, now);
// Rebuild the mutable state for all contacts // Rebuild the mutable state for all contacts
inContexts.clear(); inContexts.clear();
@@ -285,12 +399,14 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private static class TagContext { private static class TagContext {
private final KeySetId keySetId;
private final ContactId contactId; private final ContactId contactId;
private final MutableIncomingKeys inKeys; private final MutableIncomingKeys inKeys;
private final long streamNumber; private final long streamNumber;
private TagContext(ContactId contactId, MutableIncomingKeys inKeys, private TagContext(KeySetId keySetId, ContactId contactId,
long streamNumber) { MutableIncomingKeys inKeys, long streamNumber) {
this.keySetId = keySetId;
this.contactId = contactId; this.contactId = contactId;
this.inKeys = inKeys; this.inKeys = inKeys;
this.streamNumber = streamNumber; this.streamNumber = streamNumber;
@@ -299,11 +415,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
private static class RotationResult { private static class RotationResult {
private final Map<ContactId, TransportKeys> current, rotated; private final Collection<KeySet> current = new ArrayList<>();
private final Collection<KeySet> rotated = new ArrayList<>();
private RotationResult() {
current = new HashMap<>();
rotated = new HashMap<>();
}
} }
} }

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

View File

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

View File

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

View File

@@ -126,13 +126,13 @@ public class KeyEncodingAndParsingTest extends BrambleTestCase {
byte[] signature = crypto.sign("test", message, byte[] signature = crypto.sign("test", message,
privateKey.getEncoded()); privateKey.getEncoded());
// Verify the signature // Verify the signature
assertTrue(crypto.verify("test", message, publicKey.getEncoded(), assertTrue(crypto.verifySignature(signature, "test", message,
signature)); publicKey.getEncoded()));
// Encode and parse the public key - no exceptions should be thrown // Encode and parse the public key - no exceptions should be thrown
publicKey = parser.parsePublicKey(publicKey.getEncoded()); publicKey = parser.parsePublicKey(publicKey.getEncoded());
// Verify the signature again // Verify the signature again
assertTrue(crypto.verify("test", message, publicKey.getEncoded(), assertTrue(crypto.verifySignature(signature, "test", message,
signature)); publicKey.getEncoded()));
} }
@Test @Test
@@ -146,15 +146,15 @@ public class KeyEncodingAndParsingTest extends BrambleTestCase {
byte[] signature = crypto.sign("test", message, byte[] signature = crypto.sign("test", message,
privateKey.getEncoded()); privateKey.getEncoded());
// Verify the signature // Verify the signature
assertTrue(crypto.verify("test", message, publicKey.getEncoded(), assertTrue(crypto.verifySignature(signature, "test", message,
signature)); publicKey.getEncoded()));
// Encode and parse the private key - no exceptions should be thrown // Encode and parse the private key - no exceptions should be thrown
privateKey = parser.parsePrivateKey(privateKey.getEncoded()); privateKey = parser.parsePrivateKey(privateKey.getEncoded());
// Sign the data again - the signatures should be the same // Sign the data again - the signatures should be the same
byte[] signature1 = crypto.sign("test", message, byte[] signature1 = crypto.sign("test", message,
privateKey.getEncoded()); privateKey.getEncoded());
assertTrue(crypto.verify("test", message, publicKey.getEncoded(), assertTrue(crypto.verifySignature(signature1, "test", message,
signature1)); publicKey.getEncoded()));
assertArrayEquals(signature, signature1); 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.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MacTest extends BrambleTestCase { public class MacTest extends BrambleTestCase {
@@ -32,6 +33,7 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3); byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key1, input1, input2, input3); byte[] mac1 = crypto.mac(label1, key1, input1, input2, input3);
assertArrayEquals(mac, mac1); assertArrayEquals(mac, mac1);
assertTrue(crypto.verifyMac(mac, label1, key1, input1, input2, input3));
} }
@Test @Test
@@ -40,6 +42,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3); byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label2, key1, input1, input2, input3); byte[] mac1 = crypto.mac(label2, key1, input1, input2, input3);
assertFalse(Arrays.equals(mac, mac1)); 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 @Test
@@ -48,6 +55,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3); byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key2, input1, input2, input3); byte[] mac1 = crypto.mac(label1, key2, input1, input2, input3);
assertFalse(Arrays.equals(mac, mac1)); 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 @Test
@@ -57,6 +69,11 @@ public class MacTest extends BrambleTestCase {
byte[] mac = crypto.mac(label1, key1, input1, input2, input3); byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
byte[] mac1 = crypto.mac(label1, key1, input3, input2, input1); byte[] mac1 = crypto.mac(label1, key1, input3, input2, input1);
assertFalse(Arrays.equals(mac, mac1)); 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, protected abstract byte[] sign(String label, byte[] toSign,
byte[] privateKey) throws GeneralSecurityException; byte[] privateKey) throws GeneralSecurityException;
protected abstract boolean verify(String label, byte[] signedData, protected abstract boolean verify(byte[] signature, String label,
byte[] publicKey, byte[] signature) throws GeneralSecurityException; byte[] signed, byte[] publicKey) throws GeneralSecurityException;
SignatureTest() { SignatureTest() {
crypto = new CryptoComponentImpl(new TestSecureRandomProvider(), null); crypto = new CryptoComponentImpl(new TestSecureRandomProvider(), null);
@@ -85,7 +85,7 @@ public abstract class SignatureTest extends BrambleTestCase {
@Test @Test
public void testSignatureVerification() throws Exception { public void testSignatureVerification() throws Exception {
byte[] sig = sign(label, inputBytes, privateKey); byte[] sig = sign(label, inputBytes, privateKey);
assertTrue(verify(label, inputBytes, publicKey, sig)); assertTrue(verify(sig, label, inputBytes, publicKey));
} }
@Test @Test
@@ -95,7 +95,7 @@ public abstract class SignatureTest extends BrambleTestCase {
byte[] privateKey2 = k2.getPrivate().getEncoded(); byte[] privateKey2 = k2.getPrivate().getEncoded();
// calculate the signature with different key, should fail to verify // calculate the signature with different key, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey2); byte[] sig = sign(label, inputBytes, privateKey2);
assertFalse(verify(label, inputBytes, publicKey, sig)); assertFalse(verify(sig, label, inputBytes, publicKey));
} }
@Test @Test
@@ -104,7 +104,7 @@ public abstract class SignatureTest extends BrambleTestCase {
byte[] inputBytes2 = TestUtils.getRandomBytes(123); byte[] inputBytes2 = TestUtils.getRandomBytes(123);
// calculate the signature with different input, should fail to verify // calculate the signature with different input, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey); byte[] sig = sign(label, inputBytes, privateKey);
assertFalse(verify(label, inputBytes2, publicKey, sig)); assertFalse(verify(sig, label, inputBytes2, publicKey));
} }
@Test @Test
@@ -113,7 +113,7 @@ public abstract class SignatureTest extends BrambleTestCase {
String label2 = StringUtils.getRandomString(42); String label2 = StringUtils.getRandomString(42);
// calculate the signature with different label, should fail to verify // calculate the signature with different label, should fail to verify
byte[] sig = sign(label, inputBytes, privateKey); 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.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.transport.IncomingKeys; 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.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction; import org.briarproject.bramble.test.CaptureArgumentAction;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; 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.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; 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.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; 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.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_OFFERED_MESSAGES; 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.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.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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -100,27 +102,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
private final int maxLatency; private final int maxLatency;
private final ContactId contactId; private final ContactId contactId;
private final Contact contact; private final Contact contact;
private final KeySetId keySetId;
public DatabaseComponentImplTest() { public DatabaseComponentImplTest() {
clientId = new ClientId(getRandomString(123)); clientId = getClientId();
groupId = new GroupId(TestUtils.getRandomId()); group = getGroup(clientId);
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; groupId = group.getId();
group = new Group(groupId, clientId, descriptor);
author = getAuthor(); author = getAuthor();
localAuthor = getLocalAuthor(); localAuthor = getLocalAuthor();
messageId = new MessageId(TestUtils.getRandomId()); messageId = new MessageId(getRandomId());
messageId1 = new MessageId(TestUtils.getRandomId()); messageId1 = new MessageId(getRandomId());
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
size = 1234; size = 1234;
raw = new byte[size]; raw = new byte[size];
message = new Message(messageId, groupId, timestamp, raw); message = new Message(messageId, groupId, timestamp, raw);
metadata = new Metadata(); metadata = new Metadata();
metadata.put("foo", new byte[] {'b', 'a', 'r'}); metadata.put("foo", new byte[] {'b', 'a', 'r'});
transportId = new TransportId("id"); transportId = getTransportId();
maxLatency = Integer.MAX_VALUE; maxLatency = Integer.MAX_VALUE;
contactId = new ContactId(234); contactId = new ContactId(234);
contact = new Contact(contactId, author, localAuthor.getId(), contact = new Contact(contactId, author, localAuthor.getId(),
true, true); true, true);
keySetId = new KeySetId(345);
} }
private DatabaseComponent createDatabaseComponent(Database<Object> database, private DatabaseComponent createDatabaseComponent(Database<Object> database,
@@ -282,11 +285,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(18).of(database).containsContact(txn, contactId); exactly(17).of(database).containsContact(txn, contactId);
will(returnValue(false)); will(returnValue(false));
exactly(18).of(database).abortTransaction(txn); exactly(17).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
@@ -301,6 +304,16 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction); 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); transaction = db.startTransaction(false);
try { try {
db.generateAck(transaction, contactId, 123); db.generateAck(transaction, contactId, 123);
@@ -371,16 +384,6 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction); 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); transaction = db.startTransaction(false);
try { try {
db.getGroupVisibility(transaction, contactId, groupId); db.getGroupVisibility(transaction, contactId, groupId);
@@ -454,17 +457,6 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction); 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); transaction = db.startTransaction(false);
try { try {
db.setGroupVisibility(transaction, contactId, groupId, SHARED); db.setGroupVisibility(transaction, contactId, groupId, SHARED);
@@ -777,13 +769,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// endTransaction() // endTransaction()
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// Check whether the transport is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(2).of(database).containsContact(txn, contactId); oneOf(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
exactly(4).of(database).containsTransport(txn, transportId); exactly(6).of(database).containsTransport(txn, transportId);
will(returnValue(false)); will(returnValue(false));
exactly(4).of(database).abortTransaction(txn); exactly(6).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
@@ -798,6 +790,16 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.endTransaction(transaction); 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); transaction = db.startTransaction(false);
try { try {
db.getTransportKeys(transaction, transportId); db.getTransportKeys(transaction, transportId);
@@ -810,7 +812,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false); transaction = db.startTransaction(false);
try { try {
db.incrementStreamCounter(transaction, contactId, transportId, 0); db.incrementStreamCounter(transaction, transportId, keySetId);
fail(); fail();
} catch (NoSuchTransportException expected) { } catch (NoSuchTransportException expected) {
// Expected // Expected
@@ -830,7 +832,17 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
transaction = db.startTransaction(false); transaction = db.startTransaction(false);
try { 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]); new byte[REORDERING_WINDOW_SIZE / 8]);
fail(); fail();
} catch (NoSuchTransportException expected) { } catch (NoSuchTransportException expected) {
@@ -907,7 +919,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test @Test
public void testGenerateOffer() throws Exception { public void testGenerateOffer() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
@@ -938,7 +950,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test @Test
public void testGenerateRequest() throws Exception { public void testGenerateRequest() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
@@ -1126,9 +1138,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test @Test
public void testReceiveOffer() throws Exception { public void testReceiveOffer() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(getRandomId());
MessageId messageId2 = new MessageId(TestUtils.getRandomId()); MessageId messageId2 = new MessageId(getRandomId());
MessageId messageId3 = new MessageId(TestUtils.getRandomId()); MessageId messageId3 = new MessageId(getRandomId());
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1303,15 +1315,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@Test @Test
public void testTransportKeys() throws Exception { public void testTransportKeys() throws Exception {
TransportKeys transportKeys = createTransportKeys(); TransportKeys transportKeys = createTransportKeys();
Map<ContactId, TransportKeys> keys = Collection<KeySet> keys =
singletonMap(contactId, transportKeys); singletonList(new KeySet(keySetId, contactId, transportKeys));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// startTransaction() // startTransaction()
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
// updateTransportKeys() // updateTransportKeys()
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsTransport(txn, transportId); oneOf(database).containsTransport(txn, transportId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).updateTransportKeys(txn, keys); oneOf(database).updateTransportKeys(txn, keys);
@@ -1337,22 +1347,22 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
private TransportKeys createTransportKeys() { private TransportKeys createTransportKeys() {
SecretKey inPrevTagKey = TestUtils.getSecretKey(); SecretKey inPrevTagKey = getSecretKey();
SecretKey inPrevHeaderKey = TestUtils.getSecretKey(); SecretKey inPrevHeaderKey = getSecretKey();
IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey, IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
1, 123, new byte[4]); 1, 123, new byte[4]);
SecretKey inCurrTagKey = TestUtils.getSecretKey(); SecretKey inCurrTagKey = getSecretKey();
SecretKey inCurrHeaderKey = TestUtils.getSecretKey(); SecretKey inCurrHeaderKey = getSecretKey();
IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey, IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
2, 234, new byte[4]); 2, 234, new byte[4]);
SecretKey inNextTagKey = TestUtils.getSecretKey(); SecretKey inNextTagKey = getSecretKey();
SecretKey inNextHeaderKey = TestUtils.getSecretKey(); SecretKey inNextHeaderKey = getSecretKey();
IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey, IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
3, 345, new byte[4]); 3, 345, new byte[4]);
SecretKey outCurrTagKey = TestUtils.getSecretKey(); SecretKey outCurrTagKey = getSecretKey();
SecretKey outCurrHeaderKey = TestUtils.getSecretKey(); SecretKey outCurrHeaderKey = getSecretKey();
OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey, OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
2, 456); 2, 456, true);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr); return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
} }
@@ -1498,7 +1508,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testMessageDependencies() throws Exception { public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345; int shutdownHandle = 12345;
MessageId messageId2 = new MessageId(TestUtils.getRandomId()); MessageId messageId2 = new MessageId(getRandomId());
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// open() // open()
oneOf(database).open(null); oneOf(database).open(null);
@@ -1518,10 +1528,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// addMessageDependencies() // addMessageDependencies()
oneOf(database).containsMessage(txn, messageId); oneOf(database).containsMessage(txn, messageId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).addMessageDependency(txn, groupId, messageId, oneOf(database).getMessageState(txn, messageId);
messageId1); will(returnValue(DELIVERED));
oneOf(database).addMessageDependency(txn, groupId, messageId, oneOf(database).addMessageDependency(txn, message, messageId1,
messageId2); DELIVERED);
oneOf(database).addMessageDependency(txn, message, messageId2,
DELIVERED);
// getMessageDependencies() // getMessageDependencies()
oneOf(database).containsMessage(txn, messageId); oneOf(database).containsMessage(txn, messageId);
will(returnValue(true)); will(returnValue(true));

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
public class PollerTest extends BrambleMockTestCase { public class PollerTest extends BrambleMockTestCase {
@@ -48,7 +49,7 @@ public class PollerTest extends BrambleMockTestCase {
private final SecureRandom random; private final SecureRandom random;
private final Executor ioExecutor = new ImmediateExecutor(); 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 ContactId contactId = new ContactId(234);
private final int pollingInterval = 60 * 1000; private final int pollingInterval = 60 * 1000;
private final long now = System.currentTimeMillis(); private final long now = System.currentTimeMillis();
@@ -64,7 +65,7 @@ public class PollerTest extends BrambleMockTestCase {
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class); SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
SimplexPlugin simplexPlugin1 = SimplexPlugin simplexPlugin1 =
context.mock(SimplexPlugin.class, "simplexPlugin1"); context.mock(SimplexPlugin.class, "simplexPlugin1");
TransportId simplexId1 = new TransportId("simplex1"); TransportId simplexId1 = getTransportId();
List<SimplexPlugin> simplexPlugins = Arrays.asList(simplexPlugin, List<SimplexPlugin> simplexPlugins = Arrays.asList(simplexPlugin,
simplexPlugin1); simplexPlugin1);
TransportConnectionWriter simplexWriter = TransportConnectionWriter simplexWriter =
@@ -72,7 +73,7 @@ public class PollerTest extends BrambleMockTestCase {
// Two duplex plugins: one supports polling, the other doesn't // Two duplex plugins: one supports polling, the other doesn't
DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class); DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class);
TransportId duplexId = new TransportId("duplex"); TransportId duplexId = getTransportId();
DuplexPlugin duplexPlugin1 = DuplexPlugin duplexPlugin1 =
context.mock(DuplexPlugin.class, "duplexPlugin1"); context.mock(DuplexPlugin.class, "duplexPlugin1");
List<DuplexPlugin> duplexPlugins = Arrays.asList(duplexPlugin, List<DuplexPlugin> duplexPlugins = Arrays.asList(duplexPlugin,
@@ -349,7 +350,6 @@ public class PollerTest extends BrambleMockTestCase {
@Test @Test
public void testCancelsPollingOnTransportDisabled() throws Exception { public void testCancelsPollingOnTransportDisabled() throws Exception {
Plugin plugin = context.mock(Plugin.class); Plugin plugin = context.mock(Plugin.class);
List<ContactId> connected = Collections.singletonList(contactId);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
allowing(plugin).getId(); 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_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_VERSION; 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.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.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getAuthor; 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.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
@@ -51,7 +51,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
context.mock(ContactGroupFactory.class); context.mock(ContactGroupFactory.class);
private final Clock clock = context.mock(Clock.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 LocalAuthor localAuthor = getLocalAuthor();
private final BdfDictionary fooPropertiesDict = BdfDictionary.of( private final BdfDictionary fooPropertiesDict = BdfDictionary.of(
new BdfEntry("fooKey1", "fooValue1"), new BdfEntry("fooKey1", "fooValue1"),
@@ -90,7 +90,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact1 = getContact(true); Contact contact1 = getContact(true);
Contact contact2 = getContact(true); Contact contact2 = getContact(true);
List<Contact> contacts = Arrays.asList(contact1, contact2); 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() {{ context.checking(new Expectations() {{
oneOf(db).containsGroup(txn, localGroup.getId()); oneOf(db).containsGroup(txn, localGroup.getId());
@@ -143,7 +144,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testCreatesContactGroupWhenAddingContact() throws Exception { public void testCreatesContactGroupWhenAddingContact() throws Exception {
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Create the group and share it with the contact // Create the group and share it with the contact
@@ -171,7 +172,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testRemovesGroupWhenRemovingContact() throws Exception { public void testRemovesGroupWhenRemovingContact() throws Exception {
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
@@ -306,7 +307,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testStoresRemotePropertiesWithVersion0() throws Exception { public void testStoresRemotePropertiesWithVersion0() throws Exception {
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup(CLIENT_ID);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Map<TransportId, TransportProperties> properties = Map<TransportId, TransportProperties> properties =
new LinkedHashMap<>(); new LinkedHashMap<>();
@@ -399,6 +400,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId); oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(db).commitTransaction(txn); oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn); oneOf(db).endTransaction(txn);
}}); }});
@@ -417,8 +421,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact3 = getContact(true); Contact contact3 = getContact(true);
List<Contact> contacts = List<Contact> contacts =
Arrays.asList(contact1, contact2, contact3); Arrays.asList(contact1, contact2, contact3);
Group contactGroup2 = getGroup(); Group contactGroup2 = getGroup(CLIENT_ID);
Group contactGroup3 = getGroup(); Group contactGroup3 = getGroup(CLIENT_ID);
Map<MessageId, BdfDictionary> messageMetadata3 = Map<MessageId, BdfDictionary> messageMetadata3 =
new LinkedHashMap<>(); new LinkedHashMap<>();
// A remote update for another transport should be ignored // A remote update for another transport should be ignored
@@ -466,6 +470,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata3)); will(returnValue(messageMetadata3));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId); oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(db).commitTransaction(txn); oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn); oneOf(db).endTransaction(txn);
}}); }});
@@ -501,6 +508,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, updateId); oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update)); will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
// Properties are unchanged so we're done // Properties are unchanged so we're done
oneOf(db).commitTransaction(txn); oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn); oneOf(db).endTransaction(txn);
@@ -514,7 +524,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testMergingNewPropertiesCreatesUpdate() throws Exception { public void testMergingNewPropertiesCreatesUpdate() throws Exception {
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup(CLIENT_ID);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(db).startTransaction(false); oneOf(db).startTransaction(false);
@@ -549,7 +559,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
public void testMergingUpdatedPropertiesCreatesUpdate() throws Exception { public void testMergingUpdatedPropertiesCreatesUpdate() throws Exception {
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup(CLIENT_ID);
BdfDictionary oldMetadata = BdfDictionary.of( BdfDictionary oldMetadata = BdfDictionary.of(
new BdfEntry("transportId", "foo"), new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1), new BdfEntry("version", 1),
@@ -561,9 +571,12 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
MessageId contactGroupUpdateId = new MessageId(getRandomId()); MessageId contactGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> contactGroupMessageMetadata = Map<MessageId, BdfDictionary> contactGroupMessageMetadata =
Collections.singletonMap(contactGroupUpdateId, oldMetadata); 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") new BdfEntry("fooKey1", "oldFooValue1")
)); );
BdfList oldUpdate = BdfList.of("foo", 1, oldPropertiesDict);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(db).startTransaction(false); oneOf(db).startTransaction(false);
@@ -574,6 +587,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(localGroupMessageMetadata)); will(returnValue(localGroupMessageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localGroupUpdateId); oneOf(clientHelper).getMessageAsList(txn, localGroupUpdateId);
will(returnValue(oldUpdate)); will(returnValue(oldUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
oldPropertiesDict);
will(returnValue(oldProperties));
// Store the merged properties in the local group, version 2 // Store the merged properties in the local group, version 2
expectStoreMessage(txn, localGroup.getId(), "foo", expectStoreMessage(txn, localGroup.getId(), "foo",
fooPropertiesDict, 2, true, false); fooPropertiesDict, 2, true, false);
@@ -600,12 +616,6 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
t.mergeLocalProperties(new TransportId("foo"), fooProperties); 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) { private Contact getContact(boolean active) {
ContactId c = new ContactId(nextContactId++); ContactId c = new ContactId(nextContactId++);
return new Contact(c, getAuthor(), localAuthor.getId(), return new Contact(c, getAuthor(), localAuthor.getId(),
@@ -643,8 +653,14 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// Retrieve and parse the latest local properties // Retrieve and parse the latest local properties
oneOf(clientHelper).getMessageAsList(txn, fooVersion999); oneOf(clientHelper).getMessageAsList(txn, fooVersion999);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(clientHelper).getMessageAsList(txn, barVersion3); oneOf(clientHelper).getMessageAsList(txn, barVersion3);
will(returnValue(barUpdate)); 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.FormatException;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary; 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.BdfList;
import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.plugin.TransportId; 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.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; 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.api.system.Clock;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestUtils; import org.jmock.Expectations;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Mockery;
import org.junit.Test; import org.junit.Test;
import java.io.IOException; import java.io.IOException;
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH; 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; 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 TransportId transportId;
private final BdfDictionary bdfDictionary; private final BdfDictionary bdfDictionary;
private final TransportProperties transportProperties;
private final Group group; private final Group group;
private final Message message; private final Message message;
private final TransportPropertyValidator tpv; private final TransportPropertyValidator tpv;
public TransportPropertyValidatorTest() { public TransportPropertyValidatorTest() {
transportId = new TransportId("test"); transportId = getTransportId();
bdfDictionary = new BdfDictionary(); bdfDictionary = BdfDictionary.of(new BdfEntry("foo", "bar"));
transportProperties = new TransportProperties();
transportProperties.put("foo", "bar");
GroupId groupId = new GroupId(TestUtils.getRandomId()); group = getGroup(getClientId());
ClientId clientId = new ClientId(StringUtils.getRandomString(5)); message = getMessage(group.getId());
byte[] descriptor = TestUtils.getRandomBytes(12);
group = new Group(groupId, clientId, descriptor);
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); MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
Clock clock = context.mock(Clock.class); Clock clock = context.mock(Clock.class);
tpv = new TransportPropertyValidator(clientHelper, metadataEncoder, tpv = new TransportPropertyValidator(clientHelper, metadataEncoder,
clock); clock);
} }
@Test @Test
public void testValidateProperMessage() throws IOException { public void testValidateProperMessage() throws IOException {
BdfList body = BdfList.of(transportId.getString(), 4, bdfDictionary); BdfList body = BdfList.of(transportId.getString(), 4, bdfDictionary);
BdfDictionary result = tpv.validateMessage(message, group, body) context.checking(new Expectations() {{
.getDictionary(); 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()); assertEquals(4, result.getLong("version").longValue());
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateWrongVersionValue() throws IOException { public void testValidateWrongVersionValue() throws IOException {
BdfList body = BdfList.of(transportId.getString(), -1, bdfDictionary); BdfList body = BdfList.of(transportId.getString(), -1, bdfDictionary);
tpv.validateMessage(message, group, body); tpv.validateMessage(message, group, body);
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateWrongVersionType() throws IOException { public void testValidateWrongVersionType() throws IOException {
BdfList body = BdfList.of(transportId.getString(), bdfDictionary, BdfList body = BdfList.of(transportId.getString(), bdfDictionary,
bdfDictionary); bdfDictionary);
tpv.validateMessage(message, group, body); tpv.validateMessage(message, group, body);
@@ -84,27 +82,15 @@ public class TransportPropertyValidatorTest extends BrambleTestCase {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateLongTransportId() throws IOException { public void testValidateLongTransportId() throws IOException {
String wrongTransportIdString = String wrongTransportIdString =
StringUtils.getRandomString(MAX_TRANSPORT_ID_LENGTH + 1); getRandomString(MAX_TRANSPORT_ID_LENGTH + 1);
BdfList body = BdfList.of(wrongTransportIdString, 4, bdfDictionary); BdfList body = BdfList.of(wrongTransportIdString, 4, bdfDictionary);
tpv.validateMessage(message, group, body); tpv.validateMessage(message, group, body);
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateEmptyTransportId() throws IOException { public void testValidateEmptyTransportId() throws IOException {
BdfList body = BdfList.of("", 4, bdfDictionary); BdfList body = BdfList.of("", 4, bdfDictionary);
tpv.validateMessage(message, group, body); 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.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.transport.TransportConstants.PROTOCOL_VERSION; 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.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.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -73,13 +74,13 @@ public class SyncIntegrationTest extends BrambleTestCase {
component.inject(this); component.inject(this);
contactId = new ContactId(234); contactId = new ContactId(234);
transportId = new TransportId("id"); transportId = getTransportId();
// Create the transport keys // Create the transport keys
tagKey = TestUtils.getSecretKey(); tagKey = TestUtils.getSecretKey();
headerKey = TestUtils.getSecretKey(); headerKey = TestUtils.getSecretKey();
streamNumber = 123; streamNumber = 123;
// Create a group // Create a group
ClientId clientId = new ClientId(getRandomString(123)); ClientId clientId = getClientId();
int clientVersion = 1234567890; int clientVersion = 1234567890;
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
Group group = groupFactory.createGroup(clientId, clientVersion, 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.api.sync.event.MessageAddedEvent;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor; import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.ByteUtils; import org.briarproject.bramble.util.ByteUtils;
import org.briarproject.bramble.util.StringUtils;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING; 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.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 { public class ValidationManagerImplTest extends BrambleMockTestCase {
@@ -51,14 +52,12 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
private final Executor dbExecutor = new ImmediateExecutor(); private final Executor dbExecutor = new ImmediateExecutor();
private final Executor validationExecutor = new ImmediateExecutor(); private final Executor validationExecutor = new ImmediateExecutor();
private final ClientId clientId = private final ClientId clientId = getClientId();
new ClientId(StringUtils.getRandomString(5)); private final MessageId messageId = new MessageId(getRandomId());
private final MessageId messageId = new MessageId(TestUtils.getRandomId()); private final MessageId messageId1 = new MessageId(getRandomId());
private final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); private final MessageId messageId2 = new MessageId(getRandomId());
private final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); private final Group group = getGroup(clientId);
private final GroupId groupId = new GroupId(TestUtils.getRandomId()); private final GroupId groupId = group.getId();
private final byte[] descriptor = new byte[32];
private final Group group = new Group(groupId, clientId, descriptor);
private final long timestamp = System.currentTimeMillis(); private final long timestamp = System.currentTimeMillis();
private final byte[] raw = new byte[123]; private final byte[] raw = new byte[123];
private final Message message = new Message(messageId, groupId, timestamp, private final Message message = new Message(messageId, groupId, timestamp,
@@ -716,8 +715,8 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testRecursiveInvalidation() throws Exception { public void testRecursiveInvalidation() throws Exception {
MessageId messageId3 = new MessageId(TestUtils.getRandomId()); MessageId messageId3 = new MessageId(getRandomId());
MessageId messageId4 = new MessageId(TestUtils.getRandomId()); MessageId messageId4 = new MessageId(getRandomId());
Map<MessageId, State> twoDependents = new LinkedHashMap<>(); Map<MessageId, State> twoDependents = new LinkedHashMap<>();
twoDependents.put(messageId1, PENDING); twoDependents.put(messageId1, PENDING);
twoDependents.put(messageId2, PENDING); twoDependents.put(messageId2, PENDING);
@@ -819,8 +818,8 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testPendingDependentsGetDelivered() throws Exception { public void testPendingDependentsGetDelivered() throws Exception {
MessageId messageId3 = new MessageId(TestUtils.getRandomId()); MessageId messageId3 = new MessageId(getRandomId());
MessageId messageId4 = new MessageId(TestUtils.getRandomId()); MessageId messageId4 = new MessageId(getRandomId());
Message message3 = new Message(messageId3, groupId, timestamp, Message message3 = new Message(messageId3, groupId, timestamp,
raw); raw);
Message message4 = new Message(messageId4, groupId, timestamp, Message message4 = new Message(messageId4, groupId, timestamp,

View File

@@ -16,10 +16,12 @@ import javax.annotation.Nullable;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@Module @Module
public class TestPluginConfigModule { 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 public static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
@NotNullByDefault @NotNullByDefault

View File

@@ -1,18 +1,20 @@
package org.briarproject.bramble.test; package org.briarproject.bramble.test;
import org.briarproject.bramble.api.client.ClientHelper; 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.data.MetadataEncoder;
import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock; 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.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage;
public abstract class ValidatorTestCase extends BrambleMockTestCase { public abstract class ValidatorTestCase extends BrambleMockTestCase {
@@ -21,17 +23,27 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase {
protected final MetadataEncoder metadataEncoder = protected final MetadataEncoder metadataEncoder =
context.mock(MetadataEncoder.class); context.mock(MetadataEncoder.class);
protected final Clock clock = context.mock(Clock.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 Group group = getGroup(getClientId());
protected final GroupId groupId = new GroupId(getRandomId()); protected final GroupId groupId = group.getId();
protected final long timestamp = 1234567890 * 1000L; protected final byte[] descriptor = group.getDescriptor();
protected final byte[] raw = getRandomBytes(123); protected final Message message = getMessage(groupId);
protected final Message message = protected final MessageId messageId = message.getId();
new Message(messageId, groupId, timestamp, raw); protected final long timestamp = message.getTimestamp();
protected final ClientId clientId = new ClientId(getRandomString(123)); protected final byte[] raw = message.getRaw();
protected final byte[] descriptor = getRandomBytes(123); protected final Author author = getAuthor();
protected final Group group = new Group(groupId, clientId, descriptor); 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.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; 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.api.transport.StreamContext;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.concurrent.DeterministicExecutor; import org.jmock.lib.concurrent.DeterministicExecutor;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Random; 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.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getSecretKey; 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.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 DatabaseComponent db = context.mock(DatabaseComponent.class);
private final PluginConfig pluginConfig = context.mock(PluginConfig.class); private final PluginConfig pluginConfig = context.mock(PluginConfig.class);
private final TransportKeyManagerFactory transportKeyManagerFactory = private final TransportKeyManagerFactory transportKeyManagerFactory =
context.mock(TransportKeyManagerFactory.class); context.mock(TransportKeyManagerFactory.class);
private final TransportKeyManager transportKeyManager = private final TransportKeyManager transportKeyManager =
context.mock(TransportKeyManager.class); context.mock(TransportKeyManager.class);
private final DeterministicExecutor executor = new DeterministicExecutor(); private final DeterministicExecutor executor = new DeterministicExecutor();
private final Transaction txn = new Transaction(null, false); private final Transaction txn = new Transaction(null, false);
private final ContactId contactId = new ContactId(42); private final ContactId contactId = new ContactId(123);
private final ContactId inactiveContactId = new ContactId(43); private final ContactId inactiveContactId = new ContactId(234);
private final TransportId transportId = new TransportId("tId"); private final KeySetId keySetId = new KeySetId(345);
private final TransportId unknownTransportId = new TransportId("id"); private final TransportId transportId = getTransportId();
private final TransportId unknownTransportId = getTransportId();
private final StreamContext streamContext = private final StreamContext streamContext =
new StreamContext(contactId, transportId, getSecretKey(), new StreamContext(contactId, transportId, getSecretKey(),
getSecretKey(), 1); getSecretKey(), 1);
private final byte[] tag = getRandomBytes(TAG_LENGTH); private final byte[] tag = getRandomBytes(TAG_LENGTH);
public KeyManagerImplTest() { private final KeyManagerImpl keyManager = new KeyManagerImpl(db, executor,
keyManager = new KeyManagerImpl(db, executor, pluginConfig, pluginConfig, transportKeyManagerFactory);
transportKeyManagerFactory);
}
@Before @Before
public void testStartService() throws Exception { public void testStartService() throws Exception {
@@ -70,8 +70,8 @@ public class KeyManagerImplTest extends BrambleTestCase {
true, false)); true, false));
SimplexPluginFactory pluginFactory = SimplexPluginFactory pluginFactory =
context.mock(SimplexPluginFactory.class); context.mock(SimplexPluginFactory.class);
Collection<SimplexPluginFactory> factories = Collections Collection<SimplexPluginFactory> factories =
.singletonList(pluginFactory); singletonList(pluginFactory);
int maxLatency = 1337; int maxLatency = 1337;
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -110,7 +110,22 @@ public class KeyManagerImplTest extends BrambleTestCase {
}}); }});
keyManager.addContact(txn, contactId, secretKey, timestamp, alice); 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 @Test
@@ -138,7 +153,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
assertEquals(streamContext, assertEquals(streamContext,
keyManager.getStreamContext(contactId, transportId)); keyManager.getStreamContext(contactId, transportId));
context.assertIsSatisfied();
} }
@Test @Test
@@ -161,7 +175,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
assertEquals(streamContext, assertEquals(streamContext,
keyManager.getStreamContext(transportId, tag)); keyManager.getStreamContext(transportId, tag));
context.assertIsSatisfied();
} }
@Test @Test
@@ -175,8 +188,6 @@ public class KeyManagerImplTest extends BrambleTestCase {
keyManager.eventOccurred(event); keyManager.eventOccurred(event);
executor.runUntilIdle(); executor.runUntilIdle();
assertEquals(null, keyManager.getStreamContext(contactId, transportId)); assertEquals(null, keyManager.getStreamContext(contactId, transportId));
context.assertIsSatisfied();
} }
@Test @Test
@@ -196,8 +207,5 @@ public class KeyManagerImplTest extends BrambleTestCase {
keyManager.eventOccurred(event); keyManager.eventOccurred(event);
assertEquals(streamContext, assertEquals(streamContext,
keyManager.getStreamContext(inactiveContactId, transportId)); 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.plugin.TransportId;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys; 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.OutgoingKeys;
import org.briarproject.bramble.api.transport.StreamContext; import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
@@ -22,23 +24,25 @@ import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService; 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 java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; 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.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE; 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.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.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransportKeyManagerImplTest extends BrambleMockTestCase { public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@@ -50,11 +54,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
context.mock(ScheduledExecutorService.class); context.mock(ScheduledExecutorService.class);
private final Clock clock = context.mock(Clock.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 maxLatency = 30 * 1000; // 30 seconds
private final long rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE; private final long rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
private final ContactId contactId = new ContactId(123); private final ContactId contactId = new ContactId(123);
private final ContactId contactId1 = new ContactId(234); 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 tagKey = TestUtils.getSecretKey();
private final SecretKey headerKey = TestUtils.getSecretKey(); private final SecretKey headerKey = TestUtils.getSecretKey();
private final SecretKey masterKey = TestUtils.getSecretKey(); private final SecretKey masterKey = TestUtils.getSecretKey();
@@ -62,12 +69,16 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testKeysAreRotatedAtStartup() throws Exception { public void testKeysAreRotatedAtStartup() throws Exception {
Map<ContactId, TransportKeys> loaded = new LinkedHashMap<>(); TransportKeys shouldRotate = createTransportKeys(900, 0, true);
TransportKeys shouldRotate = createTransportKeys(900, 0); TransportKeys shouldNotRotate = createTransportKeys(1000, 0, true);
TransportKeys shouldNotRotate = createTransportKeys(1000, 0); TransportKeys shouldRotate1 = createTransportKeys(999, 0, false);
loaded.put(contactId, shouldRotate); Collection<KeySet> loaded = asList(
loaded.put(contactId1, shouldNotRotate); new KeySet(keySetId, contactId, shouldRotate),
TransportKeys rotated = createTransportKeys(1000, 0); 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); Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -82,6 +93,8 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(returnValue(rotated)); will(returnValue(rotated));
oneOf(transportCrypto).rotateTransportKeys(shouldNotRotate, 1000); oneOf(transportCrypto).rotateTransportKeys(shouldNotRotate, 1000);
will(returnValue(shouldNotRotate)); will(returnValue(shouldNotRotate));
oneOf(transportCrypto).rotateTransportKeys(shouldRotate1, 1000);
will(returnValue(rotated1));
// Encode the tags (3 sets per contact) // Encode the tags (3 sets per contact)
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) { for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
exactly(6).of(transportCrypto).encodeTag( exactly(6).of(transportCrypto).encodeTag(
@@ -90,8 +103,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(new EncodeTagAction()); will(new EncodeTagAction());
} }
// Save the keys that were rotated // Save the keys that were rotated
oneOf(db).updateTransportKeys(txn, oneOf(db).updateTransportKeys(txn, asList(
Collections.singletonMap(contactId, rotated)); new KeySet(keySetId, contactId, rotated),
new KeySet(keySetId2, null, rotated1))
);
// Schedule key rotation at the start of the next rotation period // Schedule key rotation at the start of the next rotation period
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with(rotationPeriodLength - 1), with(MILLISECONDS)); with(rotationPeriodLength - 1), with(MILLISECONDS));
@@ -101,18 +116,19 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId, db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency); maxLatency);
transportKeyManager.start(txn); transportKeyManager.start(txn);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
} }
@Test @Test
public void testKeysAreRotatedWhenAddingContact() throws Exception { public void testKeysAreRotatedWhenAddingContact() throws Exception {
boolean alice = random.nextBoolean(); boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(999, 0); TransportKeys transportKeys = createTransportKeys(999, 0, true);
TransportKeys rotated = createTransportKeys(1000, 0); TransportKeys rotated = createTransportKeys(1000, 0, true);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey, oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
999, alice); 999, alice, true);
will(returnValue(transportKeys)); will(returnValue(transportKeys));
// Get the current time (1 ms after start of rotation period 1000) // Get the current time (1 ms after start of rotation period 1000)
oneOf(clock).currentTimeMillis(); oneOf(clock).currentTimeMillis();
@@ -129,6 +145,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
} }
// Save the keys // Save the keys
oneOf(db).addTransportKeys(txn, contactId, rotated); oneOf(db).addTransportKeys(txn, contactId, rotated);
will(returnValue(keySetId));
}}); }});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl( TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
@@ -138,6 +155,39 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000 - 1; long timestamp = rotationPeriodLength * 1000 - 1;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp, transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice); 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 @Test
@@ -149,6 +199,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId, db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency); maxLatency);
assertNull(transportKeyManager.getStreamContext(txn, contactId)); assertNull(transportKeyManager.getStreamContext(txn, contactId));
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
} }
@Test @Test
@@ -157,29 +208,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
boolean alice = random.nextBoolean(); boolean alice = random.nextBoolean();
// The stream counter has been exhausted // The stream counter has been exhausted
TransportKeys transportKeys = createTransportKeys(1000, TransportKeys transportKeys = createTransportKeys(1000,
MAX_32_BIT_UNSIGNED + 1); MAX_32_BIT_UNSIGNED + 1, true);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{ expectAddContactNoRotation(alice, transportKeys, txn);
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);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl( TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId, db, transportCrypto, dbExecutor, scheduler, clock, transportId,
@@ -188,6 +220,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000; long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp, transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice); alice);
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId)); assertNull(transportKeyManager.getStreamContext(txn, contactId));
} }
@@ -196,30 +229,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
boolean alice = random.nextBoolean(); boolean alice = random.nextBoolean();
// The stream counter can be used one more time before being exhausted // The stream counter can be used one more time before being exhausted
TransportKeys transportKeys = createTransportKeys(1000, TransportKeys transportKeys = createTransportKeys(1000,
MAX_32_BIT_UNSIGNED); MAX_32_BIT_UNSIGNED, true);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
expectAddContactNoRotation(alice, transportKeys, txn);
context.checking(new Expectations() {{ 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 // Increment the stream counter
oneOf(db).incrementStreamCounter(txn, contactId, transportId, 1000); oneOf(db).incrementStreamCounter(txn, transportId, keySetId);
}}); }});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl( TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
@@ -230,6 +247,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
transportKeyManager.addContact(txn, contactId, masterKey, timestamp, transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice); alice);
// The first request should return a stream context // The first request should return a stream context
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
StreamContext ctx = transportKeyManager.getStreamContext(txn, StreamContext ctx = transportKeyManager.getStreamContext(txn,
contactId); contactId);
assertNotNull(ctx); assertNotNull(ctx);
@@ -239,6 +257,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
assertEquals(headerKey, ctx.getHeaderKey()); assertEquals(headerKey, ctx.getHeaderKey());
assertEquals(MAX_32_BIT_UNSIGNED, ctx.getStreamNumber()); assertEquals(MAX_32_BIT_UNSIGNED, ctx.getStreamNumber());
// The second request should return null, the counter is exhausted // The second request should return null, the counter is exhausted
assertFalse(transportKeyManager.canSendOutgoingStreams(contactId));
assertNull(transportKeyManager.getStreamContext(txn, contactId)); assertNull(transportKeyManager.getStreamContext(txn, contactId));
} }
@@ -246,29 +265,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
public void testIncomingStreamContextIsNullIfTagIsNotFound() public void testIncomingStreamContextIsNullIfTagIsNotFound()
throws Exception { throws Exception {
boolean alice = random.nextBoolean(); boolean alice = random.nextBoolean();
TransportKeys transportKeys = createTransportKeys(1000, 0); TransportKeys transportKeys = createTransportKeys(1000, 0, true);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{ expectAddContactNoRotation(alice, transportKeys, txn);
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);
}});
TransportKeyManager transportKeyManager = new TransportKeyManagerImpl( TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
db, transportCrypto, dbExecutor, scheduler, clock, transportId, db, transportCrypto, dbExecutor, scheduler, clock, transportId,
@@ -277,6 +277,8 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000; long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp, transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice); alice);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
// The tag should not be recognised
assertNull(transportKeyManager.getStreamContext(txn, assertNull(transportKeyManager.getStreamContext(txn,
new byte[TAG_LENGTH])); new byte[TAG_LENGTH]));
} }
@@ -284,14 +286,15 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testTagIsNotRecognisedTwice() throws Exception { public void testTagIsNotRecognisedTwice() throws Exception {
boolean alice = random.nextBoolean(); 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 // Keep a copy of the tags
List<byte[]> tags = new ArrayList<>(); List<byte[]> tags = new ArrayList<>();
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey, oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
1000, alice); 1000, alice, true);
will(returnValue(transportKeys)); will(returnValue(transportKeys));
// Get the current time (the start of rotation period 1000) // Get the current time (the start of rotation period 1000)
oneOf(clock).currentTimeMillis(); oneOf(clock).currentTimeMillis();
@@ -308,13 +311,14 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
will(returnValue(transportKeys)); will(returnValue(transportKeys));
// Save the keys // Save the keys
oneOf(db).addTransportKeys(txn, contactId, transportKeys); oneOf(db).addTransportKeys(txn, contactId, transportKeys);
will(returnValue(keySetId));
// Encode a new tag after sliding the window // Encode a new tag after sliding the window
oneOf(transportCrypto).encodeTag(with(any(byte[].class)), oneOf(transportCrypto).encodeTag(with(any(byte[].class)),
with(tagKey), with(PROTOCOL_VERSION), with(tagKey), with(PROTOCOL_VERSION),
with((long) REORDERING_WINDOW_SIZE)); with((long) REORDERING_WINDOW_SIZE));
will(new EncodeTagAction(tags)); will(new EncodeTagAction(tags));
// Save the reordering window (previous rotation period, base 1) // 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]); 1, new byte[REORDERING_WINDOW_SIZE / 8]);
}}); }});
@@ -325,6 +329,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
long timestamp = rotationPeriodLength * 1000; long timestamp = rotationPeriodLength * 1000;
transportKeyManager.addContact(txn, contactId, masterKey, timestamp, transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
alice); alice);
assertTrue(transportKeyManager.canSendOutgoingStreams(contactId));
// Use the first tag (previous rotation period, stream number 0) // Use the first tag (previous rotation period, stream number 0)
assertEquals(REORDERING_WINDOW_SIZE * 3, tags.size()); assertEquals(REORDERING_WINDOW_SIZE * 3, tags.size());
byte[] tag = tags.get(0); byte[] tag = tags.get(0);
@@ -344,10 +349,10 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testKeysAreRotatedToCurrentPeriod() throws Exception { public void testKeysAreRotatedToCurrentPeriod() throws Exception {
TransportKeys transportKeys = createTransportKeys(1000, 0); TransportKeys transportKeys = createTransportKeys(1000, 0, true);
Map<ContactId, TransportKeys> loaded = Collection<KeySet> loaded =
Collections.singletonMap(contactId, transportKeys); singletonList(new KeySet(keySetId, contactId, transportKeys));
TransportKeys rotated = createTransportKeys(1001, 0); TransportKeys rotated = createTransportKeys(1001, 0, true);
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Transaction txn1 = 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 // Save the keys that were rotated
oneOf(db).updateTransportKeys(txn1, 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 // Schedule key rotation at the start of the next rotation period
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with(rotationPeriodLength), with(MILLISECONDS)); with(rotationPeriodLength), with(MILLISECONDS));
@@ -406,10 +411,197 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
db, transportCrypto, dbExecutor, scheduler, clock, transportId, db, transportCrypto, dbExecutor, scheduler, clock, transportId,
maxLatency); maxLatency);
transportKeyManager.start(txn); 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, private TransportKeys createTransportKeys(long rotationPeriod,
long streamCounter) { long streamCounter, boolean active) {
IncomingKeys inPrev = new IncomingKeys(tagKey, headerKey, IncomingKeys inPrev = new IncomingKeys(tagKey, headerKey,
rotationPeriod - 1); rotationPeriod - 1);
IncomingKeys inCurr = new IncomingKeys(tagKey, headerKey, IncomingKeys inCurr = new IncomingKeys(tagKey, headerKey,
@@ -417,7 +609,7 @@ public class TransportKeyManagerImplTest extends BrambleMockTestCase {
IncomingKeys inNext = new IncomingKeys(tagKey, headerKey, IncomingKeys inNext = new IncomingKeys(tagKey, headerKey,
rotationPeriod + 1); rotationPeriod + 1);
OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey, OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
rotationPeriod, streamCounter); rotationPeriod, streamCounter, active);
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr); return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
} }

View File

@@ -352,6 +352,16 @@
/> />
</activity> </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 <activity
android:name="org.briarproject.briar.android.panic.PanicPreferencesActivity" android:name="org.briarproject.briar.android.panic.PanicPreferencesActivity"
android:label="@string/panic_setting" android:label="@string/panic_setting"
@@ -374,7 +384,12 @@
</activity> </activity>
<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"> android:theme="@android:style/Theme.NoDisplay">
</activity> </activity>

View File

@@ -62,6 +62,7 @@ import javax.inject.Inject;
import static android.app.Notification.DEFAULT_LIGHTS; import static android.app.Notification.DEFAULT_LIGHTS;
import static android.app.Notification.DEFAULT_SOUND; import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE; import static android.app.Notification.DEFAULT_VIBRATE;
import static android.app.Notification.VISIBILITY_SECRET;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 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 BLOG_POST_NOTIFICATION_ID = 6;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7; 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 long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
private static final Logger LOG = private static final Logger LOG =
@@ -174,6 +169,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
NotificationChannel nc = NotificationChannel nc =
new NotificationChannel(channelId, appContext.getString(name), new NotificationChannel(channelId, appContext.getString(name),
IMPORTANCE_DEFAULT); IMPORTANCE_DEFAULT);
nc.setLockscreenVisibility(VISIBILITY_SECRET);
nc.enableVibration(true);
nc.enableLights(true); nc.enableLights(true);
nc.setLightColor( nc.setLightColor(
ContextCompat.getColor(appContext, R.color.briar_green_light)); 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.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; 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.lifecycle.LifecycleManager.StartResult;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.logout.HideUiActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity; import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 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_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_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE; import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN; 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 AtomicBoolean created = new AtomicBoolean(false);
private final Binder binder = new BriarBinder(); private final Binder binder = new BriarBinder();
@Nullable
private BroadcastReceiver receiver = null;
@Inject @Inject
protected DatabaseConfig databaseConfig; protected DatabaseConfig databaseConfig;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@@ -140,6 +153,19 @@ public class BriarService extends Service {
stopSelf(); stopSelf();
} }
}).start(); }).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) { private void showStartupFailureNotification(StartResult result) {
@@ -184,6 +210,7 @@ public class BriarService extends Service {
super.onDestroy(); super.onDestroy();
LOG.info("Destroyed"); LOG.info("Destroyed");
stopForeground(true); stopForeground(true);
if (receiver != null) unregisterReceiver(receiver);
// Stop the services in a background thread // Stop the services in a background thread
new Thread(() -> { new Thread(() -> {
if (started) lifecycleManager.stopServices(); if (started) lifecycleManager.stopServices();
@@ -194,7 +221,48 @@ public class BriarService extends Service {
public void onLowMemory() { public void onLowMemory() {
super.onLowMemory(); super.onLowMemory();
LOG.warning("Memory is low"); 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.ShareForumMessageFragment;
import org.briarproject.briar.android.sharing.SharingModule; import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.splash.SplashScreenActivity; import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.test.TestDataActivity;
import dagger.Component; import dagger.Component;
@@ -147,6 +148,8 @@ public interface ActivityComponent {
void inject(SettingsActivity activity); void inject(SettingsActivity activity);
void inject(TestDataActivity activity);
void inject(ChangePasswordActivity activity); void inject(ChangePasswordActivity activity);
void inject(IntroductionActivity 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.DbController;
import org.briarproject.briar.android.controller.handler.UiResultHandler; import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.login.PasswordActivity; 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; import java.util.logging.Logger;

View File

@@ -781,25 +781,18 @@ public class ConversationActivity extends BriarActivity
return; return;
} }
PromptStateChangeListener listener = new PromptStateChangeListener() { PromptStateChangeListener listener = (prompt, state) -> {
@Override if (state == STATE_DISMISSED || state == STATE_FINISHED) {
public void onPromptStateChanged( introductionOnboardingSeen();
MaterialTapTargetPrompt prompt, int state) { }
if (state == STATE_DISMISSED || };
state == STATE_FINISHED) { new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
introductionOnboardingSeen(); 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)
new MaterialTapTargetPrompt.Builder(ConversationActivity.this, .show();
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"); "Unknown Request Type");
} }
loadMessages(); loadMessages();
} catch (DbException | FormatException e) { } catch (ProtocolStateException e) {
introductionResponseError(); // Action is no longer valid - reloading should solve the issue
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
LOG.log(WARNING, 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 @DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId, private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException, FormatException { boolean accept, long time) throws DbException {
if (accept) { introductionManager.respondToIntroduction(contactId, sessionId, time,
introductionManager.acceptIntroduction(contactId, sessionId, time); accept);
} else {
introductionManager.declineIntroduction(contactId, sessionId, time);
}
} }
@DatabaseExecutor @DatabaseExecutor
@@ -921,18 +913,7 @@ introductionOnboardingSeen();
@DatabaseExecutor @DatabaseExecutor
private void respondToGroupRequest(SessionId id, boolean accept) private void respondToGroupRequest(SessionId id, boolean accept)
throws DbException { throws DbException {
try { groupInvitationManager.respondToInvitation(contactId, id, accept);
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());
} }
private ListenableFutureTask<String> getContactNameTask() { private ListenableFutureTask<String> getContactNameTask() {

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AlphaAnimation; import android.view.animation.AlphaAnimation;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@@ -52,6 +54,8 @@ import javax.inject.Provider;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -84,6 +88,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
private ImageView qrCode; private ImageView qrCode;
private TextView mainProgressTitle; private TextView mainProgressTitle;
private ViewGroup mainProgressContainer; private ViewGroup mainProgressContainer;
private boolean fullscreen = false;
private boolean gotRemotePayload; private boolean gotRemotePayload;
private volatile boolean gotLocalPayload; private volatile boolean gotLocalPayload;
@@ -128,6 +133,34 @@ public class ShowQrCodeFragment extends BaseEventFragment
qrCode = view.findViewById(R.id.qr_code); qrCode = view.findViewById(R.id.qr_code);
mainProgressTitle = view.findViewById(R.id.title_progress_bar); mainProgressTitle = view.findViewById(R.id.title_progress_bar);
mainProgressContainer = view.findViewById(R.id.container_progress); 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 @Override
@@ -284,6 +317,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
new AsyncTask<Void, Void, Bitmap>() { new AsyncTask<Void, Void, Bitmap>() {
@Override @Override
@Nullable
protected Bitmap doInBackground(Void... params) { protected Bitmap doInBackground(Void... params) {
byte[] payloadBytes = payloadEncoder.encode(payload); byte[] payloadBytes = payloadEncoder.encode(payload);
if (LOG.isLoggable(INFO)) { 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.Build;
import android.os.Bundle; 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.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -7,15 +7,17 @@ import android.view.ViewGroup;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public class SignOutFragment extends BaseFragment { public class SignOutFragment extends BaseFragment {
private static final String TAG = SignOutFragment.class.getName(); public static final String TAG = SignOutFragment.class.getName();
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(@Nonnull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_sign_out, container, false); return inflater.inflate(R.layout.fragment_sign_out, container, false);
@@ -30,5 +32,4 @@ public class SignOutFragment extends BaseFragment {
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
// no need to inject // no need to inject
} }
} }

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
package org.briarproject.briar.android.settings; package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.Ringtone; import android.media.Ringtone;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v7.preference.CheckBoxPreference; import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference; import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference; 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.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.util.UserFeedback; import org.briarproject.briar.android.util.UserFeedback;
import org.briarproject.briar.api.test.TestDataCreator;
import java.util.logging.Logger; 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_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE; import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION; 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.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO; 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.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; 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.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_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM; import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP; import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
@@ -96,8 +104,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
@Inject @Inject
AndroidExecutor androidExecutor; AndroidExecutor androidExecutor;
@Inject
TestDataCreator testDataCreator;
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
@@ -132,33 +138,10 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setOnPreferenceChangeListener(this); enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this); torNetwork.setOnPreferenceChangeListener(this);
notifyPrivateMessages.setOnPreferenceChangeListener(this); if (SDK_INT >= 21) {
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
if (Build.VERSION.SDK_INT >= 21) {
notifyLockscreen.setVisible(true); notifyLockscreen.setVisible(true);
notifyLockscreen.setOnPreferenceChangeListener(this); 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( findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
preference -> { preference -> {
@@ -167,14 +150,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
}); });
Preference testData = findPreference("pref_key_test_data"); Preference testData = findPreference("pref_key_test_data");
if (IS_DEBUG_BUILD) { if (!IS_DEBUG_BUILD) {
testData.setOnPreferenceClickListener(preference -> {
LOG.info("Creating test data");
testDataCreator.createTestData();
getActivity().finish();
return true;
});
} else {
testData.setVisible(false); testData.setVisible(false);
} }
@@ -220,36 +196,54 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setValue(Boolean.toString(btSetting)); enableBluetooth.setValue(Boolean.toString(btSetting));
torNetwork.setValue(Integer.toString(torSetting)); torNetwork.setValue(Integer.toString(torSetting));
notifyPrivateMessages.setChecked(settings.getBoolean( if (SDK_INT < 26) {
PREF_NOTIFY_PRIVATE, true)); notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean( notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true)); PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
notifyForumPosts.setChecked(settings.getBoolean( PREF_NOTIFY_FORUM, true));
PREF_NOTIFY_FORUM, true)); notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyBlogPosts.setChecked(settings.getBoolean( notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true)); PREF_NOTIFY_VIBRATION, true));
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyVibration.setChecked(settings.getBoolean( notifyGroupMessages.setOnPreferenceChangeListener(this);
PREF_NOTIFY_VIBRATION, true)); notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyLockscreen.setChecked(settings.getBoolean( notifyVibration.setOnPreferenceChangeListener(this);
PREF_NOTIFY_LOCK_SCREEN, false)); notifyLockscreen.setChecked(settings.getBoolean(
PREF_NOTIFY_LOCK_SCREEN, false));
String text; notifySound.setOnPreferenceClickListener(
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) { pref -> onNotificationSoundClicked());
String ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME); String text;
if (StringUtils.isNullOrEmpty(ringtoneName)) { if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
text = getString(R.string.notify_sound_setting_default); String ringtoneName =
settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else { } else {
text = ringtoneName; text = getString(R.string.notify_sound_setting_disabled);
} }
notifySound.setSummary(text);
} else { } 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); setSettingsEnabled(true);
}); });
} }
@@ -266,6 +260,41 @@ public class SettingsFragment extends PreferenceFragmentCompat
notifySound.setEnabled(enabled); 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() { private void triggerFeedback() {
androidExecutor.runOnBackgroundThread(() -> ACRA.getErrorReporter() androidExecutor.runOnBackgroundThread(() -> ACRA.getErrorReporter()
.handleException(new UserFeedback(), false)); .handleException(new UserFeedback(), false));

View File

@@ -1,14 +1,20 @@
package org.briarproject.briar.android.splash; package org.briarproject.briar.android.splash;
import android.app.Activity; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; 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 org.briarproject.briar.R;
import static android.content.Intent.ACTION_VIEW;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
public class ExpiredActivity extends Activity { public class ExpiredActivity extends AppCompatActivity
implements OnClickListener {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
@@ -17,5 +23,13 @@ public class ExpiredActivity extends Activity {
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
setContentView(R.layout.activity_expired); 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.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -26,7 +25,6 @@ public class TextAvatarView extends FrameLayout {
private final AppCompatTextView character; private final AppCompatTextView character;
private final CircleImageView background; private final CircleImageView background;
private final TextView badge; private final TextView badge;
private int unreadCount;
public TextAvatarView(Context context, @Nullable AttributeSet attrs) { public TextAvatarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -49,30 +47,14 @@ public class TextAvatarView extends FrameLayout {
} }
public void setUnreadCount(int count) { public void setUnreadCount(int count) {
unreadCount = count;
if (count > 0) { if (count > 0) {
badge.setBackgroundResource(R.drawable.bubble);
badge.setText(String.valueOf(count)); badge.setText(String.valueOf(count));
badge.setTextColor(ContextCompat.getColor(getContext(),
R.color.briar_text_primary_inverse));
badge.setVisibility(VISIBLE); badge.setVisibility(VISIBLE);
} else { } else {
badge.setVisibility(INVISIBLE); 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) { public void setBackgroundBytes(byte[] bytes) {
int r = getByte(bytes, 0) * 3 / 4 + 96; int r = getByte(bytes, 0) * 3 / 4 + 96;
int g = getByte(bytes, 1) * 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_VIBRATION = "notifyVibration";
String PREF_NOTIFY_LOCK_SCREEN = "notifyLockScreen"; 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 // Content URIs for pending intents
String CONTACT_URI = "content://org.briarproject.briar/contact"; String CONTACT_URI = "content://org.briarproject.briar/contact";
String GROUP_URI = "content://org.briarproject.briar/group"; 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:weightSum="2"> android:baselineAligned="false">
<FrameLayout <LinearLayout
android:id="@+id/status_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" 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 <ProgressBar
android:id="@+id/status_container" 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:background="@android:color/background_light"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:paddingTop="@dimen/margin_large"
android:padding="@dimen/margin_medium" tools:text="Connection failed"/>
android:visibility="invisible"> </LinearLayout>
<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>
<FrameLayout <FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"/> android:layout_gravity="center"/>
<ImageView <RelativeLayout
android:id="@+id/qr_code"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:scaleType="fitCenter"
android:layout_gravity="center"/> <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> </FrameLayout>
</LinearLayout> </LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large" android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/> tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout> </RelativeLayout>
</FrameLayout> </FrameLayout>

View File

@@ -1,9 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:orientation="vertical">
android:gravity="center"
android:text="@string/expiry_date_reached" <LinearLayout
android:textSize="@dimen/text_size_large"/> 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:weightSum="2"> android:baselineAligned="false">
<FrameLayout <LinearLayout
android:id="@+id/status_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" 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 <ProgressBar
android:id="@+id/status_container" 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:background="@android:color/background_light"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:paddingTop="@dimen/margin_large"
android:padding="@dimen/margin_medium" tools:text="Connection failed"/>
android:visibility="invisible"> </LinearLayout>
<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>
<FrameLayout <FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
@@ -59,12 +55,31 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"/> android:layout_gravity="center"/>
<ImageView <RelativeLayout
android:id="@+id/qr_code"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:scaleType="fitCenter"
android:layout_gravity="center"/> <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> </FrameLayout>
</LinearLayout> </LinearLayout>
@@ -92,5 +107,4 @@
android:paddingTop="@dimen/margin_large" android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/> tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout> </RelativeLayout>
</FrameLayout> </FrameLayout>

View File

@@ -1,23 +1,46 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" 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_width="match_parent"
android:layout_height="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 <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge" style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_centerInParent="true"/> 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 <TextView
android:id="@+id/title_progress_bar" android:id="@+id/textView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/progressBar" android:layout_margin="8dp"
android:layout_centerHorizontal="true" android:text="@string/progress_title_logout"
android:paddingTop="@dimen/margin_large" android:textSize="@dimen/text_size_large"
android:text="@string/progress_title_logout"/> 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" android:layout_gravity="center"
tools:visibility="gone"/> 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 <org.briarproject.briar.android.view.LargeTextInputView
android:id="@+id/introductionMessageView" android:id="@+id/introductionMessageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:buttonText="@string/introduction_button" app:buttonText="@string/introduction_button"
app:hint="@string/introduction_message_hint" app:hint="@string/introduction_message_hint"
app:maxLines="5"/> app:maxLines="5"
tools:visibility="visible"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

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

View File

@@ -134,8 +134,10 @@
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_message">Ouzhpennañ ur gemennadenn (diret)</string> <string name="forum_share_message">Ouzhpennañ ur gemennadenn (diret)</string>
<!--Blogs--> <!--Blogs-->
<string name="blogs_remove_blog_ok">Dilemel</string>
<!--Blog Sharing--> <!--Blog Sharing-->
<!--RSS Feeds--> <!--RSS Feeds-->
<string name="blogs_rss_remove_feed_ok">Dilemel</string>
<!--Settings Network--> <!--Settings Network-->
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="lock_setting_title">Digevreañ</string> <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_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_notification_text">Toqueu per obtenir més informació.</string>
<string name="startup_failed_activity_title">Error iniciant Briar</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_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> <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"> <plurals name="expiry_warning">
@@ -43,6 +41,8 @@
</plurals> </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_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="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--> <!--Navigation Drawer-->
<string name="nav_drawer_open_description">Obre el calaix de navegació</string> <string name="nav_drawer_open_description">Obre el calaix de navegació</string>
<string name="nav_drawer_close_description">Tanca 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="help">Ajuda</string>
<string name="sorry">Ens sap greu</string> <string name="sorry">Ens sap greu</string>
<!--Contacts and Private Conversations--> <!--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="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="message_hint">Escriu el missatge.</string>
<string name="delete_contact">Suprimeix contacte</string> <string name="delete_contact">Suprimeix contacte</string>
<string name="dialog_title_delete_contact">Confirma la supressió del 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> <item quantity="other">S\'ha afegit %d nous contactes.</item>
</plurals> </plurals>
<!--Private Groups--> <!--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> <string name="groups_created_by">Creat per 1%s</string>
<plurals name="messages"> <plurals name="messages">
<item quantity="one">1%d missatge</item> <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_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> <string name="groups_reveal_invisible">La relació del contacte no és visible per al grup</string>
<!--Forums--> <!--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="create_forum_title">Crea un fòrum</string>
<string name="choose_forum_hint">Trieu un nom per al 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="create_forum_button">Crea el fòrum</string>
<string name="forum_created_toast">S\'ha creat 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> <string name="no_posts">No hi ha publicacions</string>
<plurals name="posts"> <plurals name="posts">
<item quantity="one">%d publicacio</item> <item quantity="one">%d publicacio</item>
@@ -219,23 +215,17 @@
<string name="btn_reply">Respon</string> <string name="btn_reply">Respon</string>
<string name="forum_leave">Abandona el fòrum</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_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="dialog_button_leave">Abandona</string>
<string name="forum_left_toast">Abandona el fòrum</string>
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_button">Comparteix el fòrum</string> <string name="forum_share_button">Comparteix el fòrum</string>
<string name="contacts_selected">Contactes seleccionats</string> <string name="contacts_selected">Contactes seleccionats</string>
<string name="activity_share_toolbar_header">Tria contactes</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_shared_snackbar">Fòrum compartit amb els contactes seleccionats</string>
<string name="forum_share_message">Afegiu un missatge (opcional)</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_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_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_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_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="shared_by_format">Compartit per 1%s</string>
<string name="forum_invitation_already_sharing">Ja esteu compartint</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> <string name="forum_invitation_response_accepted_sent">Heu acceptat la invitació del fòrum de %s.</string>
@@ -251,19 +241,15 @@
</plurals> </plurals>
<string name="nobody">Ningú</string> <string name="nobody">Ningú</string>
<!--Blogs--> <!--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="read_more">llegir més</string>
<string name="blogs_write_blog_post">Escriviu una publicació de blog</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_publish_blog_post">Publica</string>
<string name="blogs_blog_post_created">S\'ha creat la publicació al blog</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_received">S\'ha rebut una nova publicació al blog</string>
<string name="blogs_blog_post_scroll_to">Desplaça</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">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">Eliminar</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_reblog_comment_hint">Afegiu un comentari (opcional)</string> <string name="blogs_reblog_comment_hint">Afegiu un comentari (opcional)</string>
<string name="blogs_reblog_button">Rebloga</string> <string name="blogs_reblog_button">Rebloga</string>
<!--Blog Sharing--> <!--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_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_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_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> <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--> <!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importa canal RSS</string> <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_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Darrera actualització:</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">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">Eliminar</string>
<string name="blogs_rss_remove_feed_ok">Elimina el canal</string>
<string name="blogs_rss_feeds_manage_delete_error">El canal no s\'ha pogut esborrar.</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> <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--> <!--Settings Network-->
<string name="network_settings_title">Xarxes</string> <string name="network_settings_title">Xarxes</string>

View File

@@ -99,9 +99,7 @@
<string name="fix">Opravit</string> <string name="fix">Opravit</string>
<string name="help">Pomoc</string> <string name="help">Pomoc</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">Zdá se, že tu jste noví a dosud nemáte žádné kontakty.\n\nKliknětě na + v horní části a sledujte instrukce jak přidat kontakty.\n\n Prosím pamatuj: Můžeš přidat pouze nové kontakty, se kterými jsi tváří-tvář. Je to prevence situacím, kdy se za Tebe bude chtít někdo vydávat, či bude chtít číst Tvé zprávy.</string>
<string name="date_no_private_messages">Žádné zprávy</string> <string name="date_no_private_messages">Žádné zprávy</string>
<string name="no_private_messages">Toto je konverzace a její náhled.\n\nZdá se, že postrádáte konverzace.\n\nKliknutím na vstupní pole v dolní části zahajte konverzaci.</string>
<string name="message_hint">Psát zprávu</string> <string name="message_hint">Psát zprávu</string>
<string name="delete_contact">Odstranit kontakt</string> <string name="delete_contact">Odstranit kontakt</string>
<string name="dialog_title_delete_contact">Potvrdit odstranění kontaktu</string> <string name="dialog_title_delete_contact">Potvrdit odstranění kontaktu</string>
@@ -149,7 +147,6 @@
<item quantity="other">%d nových kontaktů bylo přidáno.</item> <item quantity="other">%d nových kontaktů bylo přidáno.</item>
</plurals> </plurals>
<!--Private Groups--> <!--Private Groups-->
<string name="groups_list_empty">Nepodílíte se na žádné skupině.\n\nKlikněte na ikonu + výše pro vytvoření vaší skupiny nebo požádejte některého z vašich kontaktů o pozvání do některých skupin.</string>
<string name="groups_created_by">Vytvořeno %s</string> <string name="groups_created_by">Vytvořeno %s</string>
<plurals name="messages"> <plurals name="messages">
<item quantity="one">%d zpráva</item> <item quantity="one">%d zpráva</item>
@@ -204,12 +201,10 @@
<string name="groups_reveal_visible_revealed_by_contact">Vztah s kontaktem je viditelný ve skupině (byl odkrytý %s)</string> <string name="groups_reveal_visible_revealed_by_contact">Vztah s kontaktem je viditelný ve skupině (byl odkrytý %s)</string>
<string name="groups_reveal_invisible">Vztah s kontaktem není viditelný ve skupině</string> <string name="groups_reveal_invisible">Vztah s kontaktem není viditelný ve skupině</string>
<!--Forums--> <!--Forums-->
<string name="no_forums">Dosud nemáte žádné fórum.\n\nProč si nějaké své nevytvoříte kliknutím v horní části na symbol +?\n\nMůžete taktéž požádat své kontakty o sdílení některých fór s vámi.</string>
<string name="create_forum_title">Vytvořit fórum</string> <string name="create_forum_title">Vytvořit fórum</string>
<string name="choose_forum_hint">Vybrat jméno pro vaše fórum</string> <string name="choose_forum_hint">Vybrat jméno pro vaše fórum</string>
<string name="create_forum_button">Vytvořit fórum</string> <string name="create_forum_button">Vytvořit fórum</string>
<string name="forum_created_toast">Fórum vytvořeno</string> <string name="forum_created_toast">Fórum vytvořeno</string>
<string name="no_forum_posts">Toto fórum je prázdné.\n\nPoužij ikonu tužky nahoře k vytvoření prvního příspěvku.\n\nCítíš se tu dobře? Sdílej toto fórum s vícero svými kontakty.</string>
<string name="no_posts">Žádné příspěvky</string> <string name="no_posts">Žádné příspěvky</string>
<plurals name="posts"> <plurals name="posts">
<item quantity="one">%d příspěvek</item> <item quantity="one">%d příspěvek</item>
@@ -222,23 +217,17 @@
<string name="btn_reply">Odpověď</string> <string name="btn_reply">Odpověď</string>
<string name="forum_leave">Opustit fórum</string> <string name="forum_leave">Opustit fórum</string>
<string name="dialog_title_leave_forum">Potvrdit opuštění fóra</string> <string name="dialog_title_leave_forum">Potvrdit opuštění fóra</string>
<string name="dialog_message_leave_forum">Opravdu chcete opustit toto fórum? Kontakty, které jste sdíleli s tímto fórem, mohou být odebrány z příjmu aktualizací tohoto fóra.</string>
<string name="dialog_button_leave">Opustit</string> <string name="dialog_button_leave">Opustit</string>
<string name="forum_left_toast">Opustit fórum</string>
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_button">Sdílet fórum</string> <string name="forum_share_button">Sdílet fórum</string>
<string name="contacts_selected">Zvolené kontakty</string> <string name="contacts_selected">Zvolené kontakty</string>
<string name="activity_share_toolbar_header">Zvolit kontakty</string> <string name="activity_share_toolbar_header">Zvolit kontakty</string>
<string name="no_contacts_selector">Zdá se, že jste tu noví a doposud nemáte žádné kontakty.\n\nProsím, vraťte se zpět po přidání svého prvního kontaktu.</string>
<string name="forum_shared_snackbar">Fórum sdíleno s vybranými kontakty</string> <string name="forum_shared_snackbar">Fórum sdíleno s vybranými kontakty</string>
<string name="forum_share_message">Přidat zprávu (volitelné)</string> <string name="forum_share_message">Přidat zprávu (volitelné)</string>
<string name="forum_share_error">Při sdílení tohoto fóra nastala chyba.</string> <string name="forum_share_error">Při sdílení tohoto fóra nastala chyba.</string>
<string name="forum_invitation_received">%1$s sdílel fórum \"%2$s\" s vámi.</string> <string name="forum_invitation_received">%1$s sdílel fórum \"%2$s\" s vámi.</string>
<string name="forum_invitation_sent">Sdíleli jste fórum \"%1$s\" s %2$s.</string> <string name="forum_invitation_sent">Sdíleli jste fórum \"%1$s\" s %2$s.</string>
<string name="forum_invitations_title">Pozvání do fóra</string> <string name="forum_invitations_title">Pozvání do fóra</string>
<string name="forum_invitation_exists">Již jste přijali pozvání do tohoto fóra. Přijímání dalších pozvánek povede a posílí komunikaci ve fóru.</string>
<string name="forum_joined_toast">Vstoupit do fóra</string>
<string name="forum_declined_toast">Pozvánka do fóra byla zamítnuta</string>
<string name="shared_by_format">Sdíleno %s</string> <string name="shared_by_format">Sdíleno %s</string>
<string name="forum_invitation_already_sharing">Již sdílené</string> <string name="forum_invitation_already_sharing">Již sdílené</string>
<string name="forum_invitation_response_accepted_sent">Přijali jste pozvání do fóra od %s.</string> <string name="forum_invitation_response_accepted_sent">Přijali jste pozvání do fóra od %s.</string>
@@ -255,19 +244,14 @@
</plurals> </plurals>
<string name="nobody">Nikdo</string> <string name="nobody">Nikdo</string>
<!--Blogs--> <!--Blogs-->
<string name="blogs_other_blog_empty_state">Tento blog je momentálně prázdný.\n\nPokud autor nebo osoba, sdílící s vámi tento blog, ještě nic nenapsali, pro synchronizaci příspěvků je tedy nutné, aby byli online.</string>
<string name="read_more">Číst dál</string> <string name="read_more">Číst dál</string>
<string name="blogs_write_blog_post">Napsat příspěvek do Blogu</string> <string name="blogs_write_blog_post">Napsat příspěvek do Blogu</string>
<string name="blogs_write_blog_post_body_hint">Svůj příspěvek do blogu zadejte zde</string>
<string name="blogs_publish_blog_post">Publikovat</string> <string name="blogs_publish_blog_post">Publikovat</string>
<string name="blogs_blog_post_created">Příspěvek do blogu byl vytvořen</string> <string name="blogs_blog_post_created">Příspěvek do blogu byl vytvořen</string>
<string name="blogs_blog_post_received">Přijatý nový příspěvek v blogu.</string> <string name="blogs_blog_post_received">Přijatý nový příspěvek v blogu.</string>
<string name="blogs_blog_post_scroll_to">Přejít na</string> <string name="blogs_blog_post_scroll_to">Přejít na</string>
<string name="blogs_feed_empty_state">Toto je globální kanál blogu.\n\nZdá se, že ještě nikdo neblogoval..\n\nBuďte první, klikněte na ikonu tužky a napište do blogu nový příspěvek.</string>
<string name="blogs_remove_blog">Odstranit blog</string> <string name="blogs_remove_blog">Odstranit blog</string>
<string name="blogs_remove_blog_dialog_message">Jste si jisti, že chcete odstranit tento blog a všechny příspěvky v něm?\nPamatujte, že blog nebude odstraněn ze zařízení ostatních lidí.</string> <string name="blogs_remove_blog_ok">Odstranit</string>
<string name="blogs_remove_blog_ok">Odstranit blog</string>
<string name="blogs_blog_removed">Blog byl odstraněn</string>
<string name="blogs_reblog_comment_hint">Přidat komentář (volitelné)</string> <string name="blogs_reblog_comment_hint">Přidat komentář (volitelné)</string>
<string name="blogs_reblog_button">Reblog</string> <string name="blogs_reblog_button">Reblog</string>
<!--Blog Sharing--> <!--Blog Sharing-->
@@ -282,8 +266,6 @@
<string name="blogs_sharing_invitation_received">%1$s sdílel blog \"%2$s\" s vámi.</string> <string name="blogs_sharing_invitation_received">%1$s sdílel blog \"%2$s\" s vámi.</string>
<string name="blogs_sharing_invitation_sent">Sdíleli jste blog \"%1$s\" s %2$s.</string> <string name="blogs_sharing_invitation_sent">Sdíleli jste blog \"%1$s\" s %2$s.</string>
<string name="blogs_sharing_invitations_title">Pozvánky do blogu</string> <string name="blogs_sharing_invitations_title">Pozvánky do blogu</string>
<string name="blogs_sharing_joined_toast">Přihlásit se k blogu</string>
<string name="blogs_sharing_declined_toast">Pozvání do blogu bylo zamítnuto</string>
<string name="sharing_status_blog">Kdokoli, kdo odebírá blog ho může sdílet se svými kontakty. Sdílíte tento blog s následujícími kontakty. Mohou existovat i jiní účastníci, které nevidíte.</string> <string name="sharing_status_blog">Kdokoli, kdo odebírá blog ho může sdílet se svými kontakty. Sdílíte tento blog s následujícími kontakty. Mohou existovat i jiní účastníci, které nevidíte.</string>
<!--RSS Feeds--> <!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Import RSS kanálu</string> <string name="blogs_rss_feeds_import">Import RSS kanálu</string>
@@ -295,10 +277,8 @@
<string name="blogs_rss_feeds_manage_author">Autor:</string> <string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Naposledy aktualizováno:</string> <string name="blogs_rss_feeds_manage_updated">Naposledy aktualizováno:</string>
<string name="blogs_rss_remove_feed">Odstranit kanál</string> <string name="blogs_rss_remove_feed">Odstranit kanál</string>
<string name="blogs_rss_remove_feed_dialog_message">Skutečně chceš smazat tento kanál a s ním spojené příspěvky?\nVeškeré příspěvky, které jste sdíleli, nebudou odstraněny ze zařízení jiných lidí.</string> <string name="blogs_rss_remove_feed_ok">Odstranit</string>
<string name="blogs_rss_remove_feed_ok">Odstranit kanál</string>
<string name="blogs_rss_feeds_manage_delete_error">Kanál nemohl být odstraněn !</string> <string name="blogs_rss_feeds_manage_delete_error">Kanál nemohl být odstraněn !</string>
<string name="blogs_rss_feeds_manage_empty_state">Nemáte naimportovaný žádný RSS kanál.\n\nProč nekliknete na znak plus v horní části obrazovky, kde můžete přidat svůj první zdroj?</string>
<string name="blogs_rss_feeds_manage_error">Vyskytl se problém s načtením vašeho kanálu příspěvků. Zkuste to prosím později.</string> <string name="blogs_rss_feeds_manage_error">Vyskytl se problém s načtením vašeho kanálu příspěvků. Zkuste to prosím později.</string>
<!--Settings Network--> <!--Settings Network-->
<string name="network_settings_title">Sítě</string> <string name="network_settings_title">Sítě</string>

View File

@@ -94,9 +94,7 @@
<string name="fix">Behoben</string> <string name="fix">Behoben</string>
<string name="help">Hilfe</string> <string name="help">Hilfe</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">Du scheinst hier neu zu sein und noch keine Kontakte zu haben.\n\nTippe auf das \"+\"-Symbol am oberen Bildschirmrand und folge den Anweisungen, um Freunde zu deiner Kontaktliste hinzuzufügen.\n\nDenke daran, dass du neue Kontakte nur dann hinzufügen kannst, wenn du ihnen persönlich begegnest. Das hindert andere daran, sich als deine Person auszugeben oder deine Nachrichten zu lesen.</string>
<string name="date_no_private_messages">Keine Nachrichten.</string> <string name="date_no_private_messages">Keine Nachrichten.</string>
<string name="no_private_messages">Das ist die Gesprächsansicht.\n\nEs scheint hier einen Mangel an Gesprächsthemen zu geben.\n\nUm ein neues Gespräch zu beginnen, verwende einfach das Eingabefeld am unteren Bildschirmrand.</string>
<string name="message_hint">Nachricht eingeben</string> <string name="message_hint">Nachricht eingeben</string>
<string name="delete_contact">Kontakt löschen</string> <string name="delete_contact">Kontakt löschen</string>
<string name="dialog_title_delete_contact">Löschen des Kontakts bestätigen</string> <string name="dialog_title_delete_contact">Löschen des Kontakts bestätigen</string>
@@ -143,7 +141,6 @@
<item quantity="other">%d neue Kontakte hinzugefügt.</item> <item quantity="other">%d neue Kontakte hinzugefügt.</item>
</plurals> </plurals>
<!--Private Groups--> <!--Private Groups-->
<string name="groups_list_empty">Du bist in keiner Gruppe Mitglied.\n\nTippe auf das \"+\"-Symbol am oberen Bildschirmrand, um eine Gruppe zu erstellen, oder frage deine Kontakte um Aufnahme in einer ihrer Gruppen.</string>
<string name="groups_created_by">Erstellt durch %s</string> <string name="groups_created_by">Erstellt durch %s</string>
<plurals name="messages"> <plurals name="messages">
<item quantity="one">%d Nachrichten</item> <item quantity="one">%d Nachrichten</item>
@@ -196,12 +193,10 @@
<string name="groups_reveal_visible_revealed_by_contact">Verbindung zum Kontakt ist für diese Gruppe sichtbar (offengelegt durch %s)</string> <string name="groups_reveal_visible_revealed_by_contact">Verbindung zum Kontakt ist für diese Gruppe sichtbar (offengelegt durch %s)</string>
<string name="groups_reveal_invisible">Verbindung zum Kontakt ist für diese Gruppe nicht sichtbar</string> <string name="groups_reveal_invisible">Verbindung zum Kontakt ist für diese Gruppe nicht sichtbar</string>
<!--Forums--> <!--Forums-->
<string name="no_forums">Du hast noch keine Foren.\n\nWarum erstellst du nicht einfach selbst ein neues Forum, indem du auf das \"+\"-Symbol am oberen Bildschirmrand tippst?\n\nDu kannst auch deine Kontakte auffordern, Foren mit dir zu teilen.</string>
<string name="create_forum_title">Forum erstellen</string> <string name="create_forum_title">Forum erstellen</string>
<string name="choose_forum_hint">Wähle einen Namen für dein Forum</string> <string name="choose_forum_hint">Wähle einen Namen für dein Forum</string>
<string name="create_forum_button">Forum erstellen</string> <string name="create_forum_button">Forum erstellen</string>
<string name="forum_created_toast">Forum wurde erstellt</string> <string name="forum_created_toast">Forum wurde erstellt</string>
<string name="no_forum_posts">Dieses Forum ist leer.\n\nBenutze das Stift-Symbol am oberen Bildschirmrand, um den ersten Beitrag zu verfassen.\n\nFühlst du dich einsam hier? Dann teile das Forum mit weiteren Kontakten!</string>
<string name="no_posts">Keine Beiträge</string> <string name="no_posts">Keine Beiträge</string>
<plurals name="posts"> <plurals name="posts">
<item quantity="one">%d Beitrag</item> <item quantity="one">%d Beitrag</item>
@@ -213,23 +208,17 @@
<string name="btn_reply">Antworten</string> <string name="btn_reply">Antworten</string>
<string name="forum_leave">Forum verlassen</string> <string name="forum_leave">Forum verlassen</string>
<string name="dialog_title_leave_forum">Verlassen des Forums bestätigen</string> <string name="dialog_title_leave_forum">Verlassen des Forums bestätigen</string>
<string name="dialog_message_leave_forum">Bist du sicher, dass du dieses Forum verlassen willst? Kontakte, die du mit diesem Forum geteilt hast, werden keine Updates von diesem Forum mehr bekommen.</string>
<string name="dialog_button_leave">Verlassen</string> <string name="dialog_button_leave">Verlassen</string>
<string name="forum_left_toast">Forum wurde verlassen</string>
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_button">Forum teilen</string> <string name="forum_share_button">Forum teilen</string>
<string name="contacts_selected">Ausgewählte Kontakte</string> <string name="contacts_selected">Ausgewählte Kontakte</string>
<string name="activity_share_toolbar_header">Kontakte auswählen</string> <string name="activity_share_toolbar_header">Kontakte auswählen</string>
<string name="no_contacts_selector">Du scheinst hier neu zu sein und noch keine Kontakte zu haben.\n\nBitte komm zurück, wenn du deinen ersten Kontakt hinzugefügt hast.</string>
<string name="forum_shared_snackbar">Forum mit gewählten Kontakten geteilt</string> <string name="forum_shared_snackbar">Forum mit gewählten Kontakten geteilt</string>
<string name="forum_share_message">Nachricht hinzufügen (optional)</string> <string name="forum_share_message">Nachricht hinzufügen (optional)</string>
<string name="forum_share_error">Es gab einen Fehler beim Versuch, dieses Forum zu teilen.</string> <string name="forum_share_error">Es gab einen Fehler beim Versuch, dieses Forum zu teilen.</string>
<string name="forum_invitation_received">%1$s hat das Forum \"%2$s\" mit dir geteilt.</string> <string name="forum_invitation_received">%1$s hat das Forum \"%2$s\" mit dir geteilt.</string>
<string name="forum_invitation_sent">Du hast das Forum \"%1$s\" mit %2$s geteilt.</string> <string name="forum_invitation_sent">Du hast das Forum \"%1$s\" mit %2$s geteilt.</string>
<string name="forum_invitations_title">Foreneinladungen</string> <string name="forum_invitations_title">Foreneinladungen</string>
<string name="forum_invitation_exists">Du hast bereits eine Einladung zu diesem Forum angenommen. Wenn du weitere Einladungen annimmst, wird die Kommunikation in dem Forum einfacher und das Forum wird wachsen.</string>
<string name="forum_joined_toast">Dem Forum beigetreten</string>
<string name="forum_declined_toast">Foreneinladung abgelehnt</string>
<string name="shared_by_format">Geteilt durch %s</string> <string name="shared_by_format">Geteilt durch %s</string>
<string name="forum_invitation_already_sharing">Bereits geteilt.</string> <string name="forum_invitation_already_sharing">Bereits geteilt.</string>
<string name="forum_invitation_response_accepted_sent">Du hast die Forumseinladung von %s akzeptiert.</string> <string name="forum_invitation_response_accepted_sent">Du hast die Forumseinladung von %s akzeptiert.</string>
@@ -245,19 +234,14 @@
</plurals> </plurals>
<string name="nobody">Niemand</string> <string name="nobody">Niemand</string>
<!--Blogs--> <!--Blogs-->
<string name="blogs_other_blog_empty_state">Dieser Blog ist momentan leer.\n\nEntweder hat der Autor noch nichts geschrieben, oder die Person, die diesen Blog mit dir geteilt hat, muss online gehen, sodass Beiträge synchronisiert werden können.</string>
<string name="read_more">weiterlesen</string> <string name="read_more">weiterlesen</string>
<string name="blogs_write_blog_post">Blogbeitrag erstellen</string> <string name="blogs_write_blog_post">Blogbeitrag erstellen</string>
<string name="blogs_write_blog_post_body_hint">Gib hier deinen Blogbeitrag ein</string>
<string name="blogs_publish_blog_post">Veröffentlichen</string> <string name="blogs_publish_blog_post">Veröffentlichen</string>
<string name="blogs_blog_post_created">Blogbeitrag erstellt</string> <string name="blogs_blog_post_created">Blogbeitrag erstellt</string>
<string name="blogs_blog_post_received">Neuen Blogbeitrag empfangen</string> <string name="blogs_blog_post_received">Neuen Blogbeitrag empfangen</string>
<string name="blogs_blog_post_scroll_to">Scrolle zu</string> <string name="blogs_blog_post_scroll_to">Scrolle zu</string>
<string name="blogs_feed_empty_state">Dies ist die globale Blog-Zeitleiste.\n\nOffensichtlich hat noch niemand etwas veröffentlicht.\n\nSei die oder der Erste und tippe auf das Stift-Symbol, um einen neuen Blogbeitrag zu verfassen.</string>
<string name="blogs_remove_blog">Blog entfernen</string> <string name="blogs_remove_blog">Blog entfernen</string>
<string name="blogs_remove_blog_dialog_message">Bist du sicher, dass du diesen Blog und alle dazugehörigen Beiträge löschen möchtest?\nBeachte, dass dies nicht den Blog auf Geräten anderer Leute löscht.</string> <string name="blogs_remove_blog_ok">Aufheben</string>
<string name="blogs_remove_blog_ok">Blog entfernen</string>
<string name="blogs_blog_removed">Blog wurde entfernt</string>
<string name="blogs_reblog_comment_hint">Kommentar hinzufügen (optional)</string> <string name="blogs_reblog_comment_hint">Kommentar hinzufügen (optional)</string>
<string name="blogs_reblog_button">Rebloggen</string> <string name="blogs_reblog_button">Rebloggen</string>
<!--Blog Sharing--> <!--Blog Sharing-->
@@ -272,8 +256,6 @@
<string name="blogs_sharing_invitation_received">%1$shat den Blog \"%2$s\" mit dir geteilt.</string> <string name="blogs_sharing_invitation_received">%1$shat den Blog \"%2$s\" mit dir geteilt.</string>
<string name="blogs_sharing_invitation_sent">Du teilst den Blog \"%1$s\" mit %2$s.</string> <string name="blogs_sharing_invitation_sent">Du teilst den Blog \"%1$s\" mit %2$s.</string>
<string name="blogs_sharing_invitations_title">Blogeinladungen</string> <string name="blogs_sharing_invitations_title">Blogeinladungen</string>
<string name="blogs_sharing_joined_toast">Blog abonniert</string>
<string name="blogs_sharing_declined_toast">Blogeinladung abgelehnt</string>
<string name="sharing_status_blog">Jeder Abonnent eines Blogs kann diesen mit seinen Kontakten teilen. Du teilst diesen Blog mit den folgenden Kontakten. Möglicherweise gibt es Abonnenten die nicht sichtbar sind.</string> <string name="sharing_status_blog">Jeder Abonnent eines Blogs kann diesen mit seinen Kontakten teilen. Du teilst diesen Blog mit den folgenden Kontakten. Möglicherweise gibt es Abonnenten die nicht sichtbar sind.</string>
<!--RSS Feeds--> <!--RSS Feeds-->
<string name="blogs_rss_feeds_import">RSS-Feed importieren</string> <string name="blogs_rss_feeds_import">RSS-Feed importieren</string>
@@ -285,10 +267,8 @@
<string name="blogs_rss_feeds_manage_author">Autor:</string> <string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Letzte Aktualisierung:</string> <string name="blogs_rss_feeds_manage_updated">Letzte Aktualisierung:</string>
<string name="blogs_rss_remove_feed">Feed entfernen</string> <string name="blogs_rss_remove_feed">Feed entfernen</string>
<string name="blogs_rss_remove_feed_dialog_message">Soll der Feed mit allen Posts wirklich gelöscht werden?\nGeteilte Posts werden dabei nicht von anderen Geräten gelöscht.</string> <string name="blogs_rss_remove_feed_ok">Aufheben</string>
<string name="blogs_rss_remove_feed_ok">Feed entfernen</string>
<string name="blogs_rss_feeds_manage_delete_error">Der Feed konnte nicht gelöscht werden!</string> <string name="blogs_rss_feeds_manage_delete_error">Der Feed konnte nicht gelöscht werden!</string>
<string name="blogs_rss_feeds_manage_empty_state">Du hast bisher noch keine RSS-Feeds importiert. Tippe auf das \"+\"-Symbol am oberen Bildschirmrand, um einen neuen Feed hinzuzufügen.</string>
<string name="blogs_rss_feeds_manage_error">Es gab ein Problem beim Laden deiner Feeds. Bitte versuche es später erneut.</string> <string name="blogs_rss_feeds_manage_error">Es gab ein Problem beim Laden deiner Feeds. Bitte versuche es später erneut.</string>
<!--Settings Network--> <!--Settings Network-->
<string name="network_settings_title">Netzwerke</string> <string name="network_settings_title">Netzwerke</string>

View File

@@ -33,8 +33,6 @@
<string name="startup_failed_notification_title">Briar no pudo iniciarse</string> <string name="startup_failed_notification_title">Briar no pudo iniciarse</string>
<string name="startup_failed_notification_text">Toca para más información.</string> <string name="startup_failed_notification_text">Toca para más información.</string>
<string name="startup_failed_activity_title">Fallo al iniciar Briar</string> <string name="startup_failed_activity_title">Fallo al iniciar Briar</string>
<string name="startup_failed_db_error">Por alguna razón, la base de datos de Briar se ha dañada irreparablemente. Tu cuenta, tus datos y tus contactos se han perdido. Lamentablemente, necesitas reinstalar Briar y configurar una nueva cuenta. Puedes hacerlo seleccionando «He olvidado mi contraseña» al arrancar Briar.</string>
<string name="startup_failed_data_too_old_error">Tu cuenta se creó en una versión antigua de esta apli y no puede abrirse con la versión actual. Debes instalar la versión anterior o eliminar tu cuenta eligiendo «He olvidado mi contraseña» al arrancar Briar.</string>
<string name="startup_failed_data_too_new_error">La versión de esta apli es demasiado antigua. Por favor, actualiza a la última versión y prueba de nuevo.</string> <string name="startup_failed_data_too_new_error">La versión de esta apli es demasiado antigua. Por favor, actualiza a la última versión y prueba de nuevo.</string>
<string name="startup_failed_service_error">Briar no pudo iniciar un complemento necesario. Reinstalar Briar suele solucionar el problema. Sin embargo, ten en cuenta que perderás tu cuenta y todos los datos asociados ya que Briar no almacena esta información en ningún servidor central.</string> <string name="startup_failed_service_error">Briar no pudo iniciar un complemento necesario. Reinstalar Briar suele solucionar el problema. Sin embargo, ten en cuenta que perderás tu cuenta y todos los datos asociados ya que Briar no almacena esta información en ningún servidor central.</string>
<plurals name="expiry_warning"> <plurals name="expiry_warning">
@@ -43,6 +41,8 @@
</plurals> </plurals>
<string name="expiry_update">La fecha de caducidad de las pruebas se ha ampliado. Su cuenta expirará en %d días.</string> <string name="expiry_update">La fecha de caducidad de las pruebas se ha ampliado. Su cuenta expirará en %d días.</string>
<string name="expiry_date_reached">Esta versión ha caducado.\n¡Gracias por probarla!</string> <string name="expiry_date_reached">Esta versión ha caducado.\n¡Gracias por probarla!</string>
<string name="startup_open_database">Descifrando la base de datos...</string>
<string name="startup_migrate_database">Actualizando la base de datos...</string>
<!--Navigation Drawer--> <!--Navigation Drawer-->
<string name="nav_drawer_open_description">Abrir el panel de navegación</string> <string name="nav_drawer_open_description">Abrir el panel de navegación</string>
<string name="nav_drawer_close_description">Cierra el panel de navegación</string> <string name="nav_drawer_close_description">Cierra el panel de navegación</string>
@@ -99,9 +99,7 @@
<string name="help">Ayuda</string> <string name="help">Ayuda</string>
<string name="sorry">Disculpe</string> <string name="sorry">Disculpe</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">Parece que eres nuevo por aquí y no tienes aún contactos.\n\nPulsa el signo + en la parte superior y sigue las instrucciones para añadir amigos a tu lista.\n\nPor favor, recuerda: sólo puedes añadir nuevos contactos cara a cara para evitar que nadie suplante tu identidad o lea tus mensajes en el futuro. </string>
<string name="date_no_private_messages">Sin mensajes.</string> <string name="date_no_private_messages">Sin mensajes.</string>
<string name="no_private_messages">Esta es la vista de conversación.\n\nParece que aún no hay ninguna.\n\nPulsa el campo de texto en la parte inferior para empezar la conversación.</string>
<string name="message_hint">Escribe un mensaje</string> <string name="message_hint">Escribe un mensaje</string>
<string name="delete_contact">Eliminar contacto</string> <string name="delete_contact">Eliminar contacto</string>
<string name="dialog_title_delete_contact">Confirmar eliminación de contacto</string> <string name="dialog_title_delete_contact">Confirmar eliminación de contacto</string>
@@ -149,7 +147,6 @@
<item quantity="other">%d nuevos contactos añadido</item> <item quantity="other">%d nuevos contactos añadido</item>
</plurals> </plurals>
<!--Private Groups--> <!--Private Groups-->
<string name="groups_list_empty">Aún no participas en ningún grupo.\n\nCrea un grupo pulsando en el signo + arriba o pide a tus contactos que te inviten a uno de sus grupos.</string>
<string name="groups_created_by">Creado por %s</string> <string name="groups_created_by">Creado por %s</string>
<plurals name="messages"> <plurals name="messages">
<item quantity="one">%d mensaje</item> <item quantity="one">%d mensaje</item>
@@ -202,12 +199,11 @@
<string name="groups_reveal_visible_revealed_by_contact">Las relaciones entre los contactos son visibles al grupo (las reveló %s)</string> <string name="groups_reveal_visible_revealed_by_contact">Las relaciones entre los contactos son visibles al grupo (las reveló %s)</string>
<string name="groups_reveal_invisible">Las relaciones entre los contactos no son visibles al grupo</string> <string name="groups_reveal_invisible">Las relaciones entre los contactos no son visibles al grupo</string>
<!--Forums--> <!--Forums-->
<string name="no_forums">No tienes ningún foro aún.\n\n¿Por qué no creas uno pulsando el signo + de la parte superior?\n\nTambién puedes pedirle a tus contactos que compartan foros contigo.</string>
<string name="create_forum_title">Crear foro</string> <string name="create_forum_title">Crear foro</string>
<string name="choose_forum_hint">Elige un nombre para el foro</string> <string name="choose_forum_hint">Elige un nombre para el foro</string>
<string name="create_forum_button">Crear foro</string> <string name="create_forum_button">Crear foro</string>
<string name="forum_created_toast">Foro creado</string> <string name="forum_created_toast">Foro creado</string>
<string name="no_forum_posts">Este foro está vacío.\n\nUsa el signo del lápiz de la parte superior para redactar una primera publicación.\n\n¿No te sientes un poco solo aquí? ¡Comparte este foro con más contactos!</string> <string name="no_forum_posts">No hay publicaciones para mostrar</string>
<string name="no_posts">Sin publicaciones</string> <string name="no_posts">Sin publicaciones</string>
<plurals name="posts"> <plurals name="posts">
<item quantity="one">%d publicación</item> <item quantity="one">%d publicación</item>
@@ -219,23 +215,17 @@
<string name="btn_reply">Responder</string> <string name="btn_reply">Responder</string>
<string name="forum_leave">Abandonar foro</string> <string name="forum_leave">Abandonar foro</string>
<string name="dialog_title_leave_forum">Confirmación abandono del foro</string> <string name="dialog_title_leave_forum">Confirmación abandono del foro</string>
<string name="dialog_message_leave_forum">¿Seguro que quieres abandonar el foro? Puede que los contactos que invitaste al foro dejen de recibir actualizaciones del mismo.</string>
<string name="dialog_button_leave">Abandonar</string> <string name="dialog_button_leave">Abandonar</string>
<string name="forum_left_toast">Foro abandonado</string>
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_button">Compartir foro</string> <string name="forum_share_button">Compartir foro</string>
<string name="contacts_selected">Contactos seleccionados</string> <string name="contacts_selected">Contactos seleccionados</string>
<string name="activity_share_toolbar_header">Elige contactos</string> <string name="activity_share_toolbar_header">Elige contactos</string>
<string name="no_contacts_selector">Parece que eres nuevo aquí y no tienes contactos aún.\n\nPor favor, vuelve cuando hayas añadido tu primer contacto.</string>
<string name="forum_shared_snackbar">Foro compartido con los contactos seleccionados</string> <string name="forum_shared_snackbar">Foro compartido con los contactos seleccionados</string>
<string name="forum_share_message">Añade un mensaje (opcional)</string> <string name="forum_share_message">Añade un mensaje (opcional)</string>
<string name="forum_share_error">Hubo un error compartiendo este foro.</string> <string name="forum_share_error">Hubo un error compartiendo este foro.</string>
<string name="forum_invitation_received">%1$s ha compartido el foro \"%2$s\" contigo.</string> <string name="forum_invitation_received">%1$s ha compartido el foro \"%2$s\" contigo.</string>
<string name="forum_invitation_sent">Has compartido el foro \"%1$s\" con %2$s.</string> <string name="forum_invitation_sent">Has compartido el foro \"%1$s\" con %2$s.</string>
<string name="forum_invitations_title">Invitaciones a foros</string> <string name="forum_invitations_title">Invitaciones a foros</string>
<string name="forum_invitation_exists">Ya aceptaste la invitación a este foro. Aceptar más invitaciones aumentará y fortalecerá la comunicación con el foro.</string>
<string name="forum_joined_toast">Te uniste al foro</string>
<string name="forum_declined_toast">Invitación al foro rechazada</string>
<string name="shared_by_format">Compartido por %s</string> <string name="shared_by_format">Compartido por %s</string>
<string name="forum_invitation_already_sharing">Ya se está compartiendo</string> <string name="forum_invitation_already_sharing">Ya se está compartiendo</string>
<string name="forum_invitation_response_accepted_sent">Aceptaste la invitación al foro de %s.</string> <string name="forum_invitation_response_accepted_sent">Aceptaste la invitación al foro de %s.</string>
@@ -251,19 +241,15 @@
</plurals> </plurals>
<string name="nobody">Nadie</string> <string name="nobody">Nadie</string>
<!--Blogs--> <!--Blogs-->
<string name="blogs_other_blog_empty_state">Este blog está vacío.\n\nO el autor no ha escrito aún, o la persona que compartió este blog contigo necesita conectarse para que se puedan sincronizar los artículos.</string> <string name="blogs_other_blog_empty_state">No hay publicaciones para mostrar</string>
<string name="read_more">leer más</string> <string name="read_more">leer más</string>
<string name="blogs_write_blog_post">Escribir artículo del blog</string> <string name="blogs_write_blog_post">Escribir artículo del blog</string>
<string name="blogs_write_blog_post_body_hint">Escribe tu artículo aquí</string>
<string name="blogs_publish_blog_post">Publicar</string> <string name="blogs_publish_blog_post">Publicar</string>
<string name="blogs_blog_post_created">Creado artículo del blog</string> <string name="blogs_blog_post_created">Creado artículo del blog</string>
<string name="blogs_blog_post_received">Recibido nuevo artículo del blog</string> <string name="blogs_blog_post_received">Recibido nuevo artículo del blog</string>
<string name="blogs_blog_post_scroll_to">Desplazarse hasta</string> <string name="blogs_blog_post_scroll_to">Desplazarse hasta</string>
<string name="blogs_feed_empty_state">Esta es la lista global de entradas de blogs.\n\nParece que nadie ha publicado nada todavía.\n\nSé el primero: pulsa el signo del lápiz para escribir una nueva entrada de blog.</string>
<string name="blogs_remove_blog">Eliminar blog</string> <string name="blogs_remove_blog">Eliminar blog</string>
<string name="blogs_remove_blog_dialog_message">¿Seguro que quieres eliminar este blog y todos sus artículos?\nTen en cuenta que no se eliminará el blog de los dispositivos de otras personas.</string> <string name="blogs_remove_blog_ok">Eliminar</string>
<string name="blogs_remove_blog_ok">Eliminar blog</string>
<string name="blogs_blog_removed">Blog eliminado</string>
<string name="blogs_reblog_comment_hint">Añade un comentario (opcional)</string> <string name="blogs_reblog_comment_hint">Añade un comentario (opcional)</string>
<string name="blogs_reblog_button">Rebloguear</string> <string name="blogs_reblog_button">Rebloguear</string>
<!--Blog Sharing--> <!--Blog Sharing-->
@@ -278,8 +264,6 @@
<string name="blogs_sharing_invitation_received">%1$s ha compartido el blog \"%2$s\" contigo.</string> <string name="blogs_sharing_invitation_received">%1$s ha compartido el blog \"%2$s\" contigo.</string>
<string name="blogs_sharing_invitation_sent">Has compartido el blog \"%1$s\" con %2$s.</string> <string name="blogs_sharing_invitation_sent">Has compartido el blog \"%1$s\" con %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitaciones a blogs</string> <string name="blogs_sharing_invitations_title">Invitaciones a blogs</string>
<string name="blogs_sharing_joined_toast">Suscrito al blog</string>
<string name="blogs_sharing_declined_toast">Rechazada invitación al blog</string>
<string name="sharing_status_blog">Cualquiera que se suscriba a un blog puede compartirlo con sus contactos. Estás compartiendo este blog con contactos listados a continuación. Puede haber otros suscriptores que no puedes ver.</string> <string name="sharing_status_blog">Cualquiera que se suscriba a un blog puede compartirlo con sus contactos. Estás compartiendo este blog con contactos listados a continuación. Puede haber otros suscriptores que no puedes ver.</string>
<!--RSS Feeds--> <!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importar canal RSS</string> <string name="blogs_rss_feeds_import">Importar canal RSS</string>
@@ -291,10 +275,8 @@
<string name="blogs_rss_feeds_manage_author">Autor:</string> <string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Última actualización:</string> <string name="blogs_rss_feeds_manage_updated">Última actualización:</string>
<string name="blogs_rss_remove_feed">Eliminar canal RSS</string> <string name="blogs_rss_remove_feed">Eliminar canal RSS</string>
<string name="blogs_rss_remove_feed_dialog_message">¿Seguro que quieres borrar este canal RSS y todas sus publicaciones?\nNo se eliminará ningún artículo que hayas compartido de los dispositivos de otras personas.</string> <string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_remove_feed_ok">Eliminar canal RSS</string>
<string name="blogs_rss_feeds_manage_delete_error">¡El canal no pudo ser eliminado!</string> <string name="blogs_rss_feeds_manage_delete_error">¡El canal no pudo ser eliminado!</string>
<string name="blogs_rss_feeds_manage_empty_state">No has importado ningún canal RSS.\n\n¿Por qué no pulsas el signo más de la esquina superior derecha para añadir el primero?</string>
<string name="blogs_rss_feeds_manage_error">Hubo un problema cargando tus canales RSS. Por favor, prueba más tarde.</string> <string name="blogs_rss_feeds_manage_error">Hubo un problema cargando tus canales RSS. Por favor, prueba más tarde.</string>
<!--Settings Network--> <!--Settings Network-->
<string name="network_settings_title">Redes</string> <string name="network_settings_title">Redes</string>

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